Repository: pypeclub/OpenPype
Branch: develop
Commit: f67bacf11713
Files: 3831
Total size: 27.9 MB
Directory structure:
gitextract_6pkohrst/
├── .all-contributorsrc
├── .dockerignore
├── .gitattributes
├── .github/
│ ├── ISSUE_TEMPLATE/
│ │ ├── bug_report.yml
│ │ ├── config.yml
│ │ └── enhancement_request.yml
│ ├── pr-branch-labeler.yml
│ ├── pr-glob-labeler.yml
│ ├── pull_request_template.md
│ └── workflows/
│ ├── documentation.yml
│ ├── milestone_assign.yml
│ ├── milestone_create.yml
│ ├── miletone_release_trigger.yml
│ ├── nightly_merge.yml
│ ├── pr_labels.yml
│ ├── prerelease.yml
│ ├── project_task_statuses.yml
│ ├── test_build.yml
│ └── update_bug_report.yml
├── .gitignore
├── .gitmodules
├── .hound.yml
├── .prettierrc
├── ARCHITECTURE.md
├── CHANGELOG.md
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── Dockerfile
├── Dockerfile.centos7
├── Dockerfile.debian
├── HISTORY.md
├── LICENSE
├── README.md
├── app_launcher.py
├── conftest.py
├── docs/
│ ├── Makefile
│ ├── README.md
│ ├── make.bat
│ └── source/
│ ├── _static/
│ │ └── README.md
│ ├── _templates/
│ │ └── autoapi/
│ │ ├── index.rst
│ │ └── python/
│ │ ├── attribute.rst
│ │ ├── class.rst
│ │ ├── data.rst
│ │ ├── exception.rst
│ │ ├── function.rst
│ │ ├── method.rst
│ │ ├── module.rst
│ │ ├── package.rst
│ │ └── property.rst
│ ├── conf.py
│ ├── index.rst
│ └── readme.rst
├── igniter/
│ ├── Poppins/
│ │ └── OFL.txt
│ ├── __init__.py
│ ├── __main__.py
│ ├── bootstrap_repos.py
│ ├── install_dialog.py
│ ├── install_thread.py
│ ├── message_dialog.py
│ ├── nice_progress_bar.py
│ ├── openpype.icns
│ ├── splash.txt
│ ├── stylesheet.css
│ ├── terminal_splash.py
│ ├── tools.py
│ ├── update_thread.py
│ ├── update_window.py
│ ├── user_settings.py
│ └── version.py
├── inno_setup.iss
├── openpype/
│ ├── __init__.py
│ ├── __main__.py
│ ├── addons/
│ │ └── README.md
│ ├── cli.py
│ ├── client/
│ │ ├── __init__.py
│ │ ├── entities.py
│ │ ├── entity_links.py
│ │ ├── mongo/
│ │ │ ├── __init__.py
│ │ │ ├── entities.py
│ │ │ ├── entity_links.py
│ │ │ ├── mongo.py
│ │ │ └── operations.py
│ │ ├── notes.md
│ │ ├── operations.py
│ │ ├── operations_base.py
│ │ └── server/
│ │ ├── __init__.py
│ │ ├── constants.py
│ │ ├── conversion_utils.py
│ │ ├── entities.py
│ │ ├── entity_links.py
│ │ ├── openpype_comp.py
│ │ ├── operations.py
│ │ ├── thumbnails.py
│ │ └── utils.py
│ ├── hooks/
│ │ ├── pre_add_last_workfile_arg.py
│ │ ├── pre_copy_template_workfile.py
│ │ ├── pre_create_extra_workdir_folders.py
│ │ ├── pre_global_host_data.py
│ │ ├── pre_mac_launch.py
│ │ ├── pre_new_console_apps.py
│ │ ├── pre_non_python_host_launch.py
│ │ └── pre_ocio_hook.py
│ ├── host/
│ │ ├── __init__.py
│ │ ├── dirmap.py
│ │ ├── host.py
│ │ └── interfaces.py
│ ├── hosts/
│ │ ├── __init__.py
│ │ ├── aftereffects/
│ │ │ ├── __init__.py
│ │ │ ├── addon.py
│ │ │ ├── api/
│ │ │ │ ├── README.md
│ │ │ │ ├── __init__.py
│ │ │ │ ├── extension/
│ │ │ │ │ ├── .debug
│ │ │ │ │ ├── CSXS/
│ │ │ │ │ │ └── manifest.xml
│ │ │ │ │ ├── css/
│ │ │ │ │ │ ├── boilerplate.css
│ │ │ │ │ │ └── styles.css
│ │ │ │ │ ├── index.html
│ │ │ │ │ ├── js/
│ │ │ │ │ │ ├── libs/
│ │ │ │ │ │ │ ├── CSInterface.js
│ │ │ │ │ │ │ ├── json.js
│ │ │ │ │ │ │ └── wsrpc.js
│ │ │ │ │ │ ├── main.js
│ │ │ │ │ │ └── themeManager.js
│ │ │ │ │ └── jsx/
│ │ │ │ │ └── hostscript.jsx
│ │ │ │ ├── extension.zxp
│ │ │ │ ├── launch_logic.py
│ │ │ │ ├── lib.py
│ │ │ │ ├── pipeline.py
│ │ │ │ ├── plugin.py
│ │ │ │ ├── workfile_template_builder.py
│ │ │ │ └── ws_stub.py
│ │ │ ├── plugins/
│ │ │ │ ├── __init__.py
│ │ │ │ ├── create/
│ │ │ │ │ ├── create_render.py
│ │ │ │ │ └── workfile_creator.py
│ │ │ │ ├── load/
│ │ │ │ │ ├── load_background.py
│ │ │ │ │ └── load_file.py
│ │ │ │ └── publish/
│ │ │ │ ├── add_publish_highlight.py
│ │ │ │ ├── closeAE.py
│ │ │ │ ├── collect_audio.py
│ │ │ │ ├── collect_current_file.py
│ │ │ │ ├── collect_extension_version.py
│ │ │ │ ├── collect_render.py
│ │ │ │ ├── collect_review.py
│ │ │ │ ├── collect_workfile.py
│ │ │ │ ├── extract_local_render.py
│ │ │ │ ├── extract_save_scene.py
│ │ │ │ ├── help/
│ │ │ │ │ ├── validate_footage_items.xml
│ │ │ │ │ ├── validate_instance_asset.xml
│ │ │ │ │ └── validate_scene_settings.xml
│ │ │ │ ├── increment_workfile.py
│ │ │ │ ├── pre_collect_render.py
│ │ │ │ ├── remove_publish_highlight.py
│ │ │ │ ├── validate_footage_items.py
│ │ │ │ ├── validate_instance_asset.py
│ │ │ │ └── validate_scene_settings.py
│ │ │ └── resources/
│ │ │ └── template.aep
│ │ ├── blender/
│ │ │ ├── __init__.py
│ │ │ ├── addon.py
│ │ │ ├── api/
│ │ │ │ ├── __init__.py
│ │ │ │ ├── action.py
│ │ │ │ ├── capture.py
│ │ │ │ ├── colorspace.py
│ │ │ │ ├── lib.py
│ │ │ │ ├── ops.py
│ │ │ │ ├── pipeline.py
│ │ │ │ ├── plugin.py
│ │ │ │ ├── render_lib.py
│ │ │ │ └── workio.py
│ │ │ ├── blender_addon/
│ │ │ │ └── startup/
│ │ │ │ └── init.py
│ │ │ ├── hooks/
│ │ │ │ ├── pre_add_run_python_script_arg.py
│ │ │ │ ├── pre_pyside_install.py
│ │ │ │ └── pre_windows_console.py
│ │ │ └── plugins/
│ │ │ ├── create/
│ │ │ │ ├── convert_legacy.py
│ │ │ │ ├── create_action.py
│ │ │ │ ├── create_animation.py
│ │ │ │ ├── create_blendScene.py
│ │ │ │ ├── create_camera.py
│ │ │ │ ├── create_layout.py
│ │ │ │ ├── create_model.py
│ │ │ │ ├── create_pointcache.py
│ │ │ │ ├── create_render.py
│ │ │ │ ├── create_review.py
│ │ │ │ ├── create_rig.py
│ │ │ │ └── create_workfile.py
│ │ │ ├── load/
│ │ │ │ ├── import_workfile.py
│ │ │ │ ├── load_abc.py
│ │ │ │ ├── load_action.py
│ │ │ │ ├── load_animation.py
│ │ │ │ ├── load_audio.py
│ │ │ │ ├── load_blend.py
│ │ │ │ ├── load_blendscene.py
│ │ │ │ ├── load_camera_abc.py
│ │ │ │ ├── load_camera_fbx.py
│ │ │ │ ├── load_fbx.py
│ │ │ │ ├── load_layout_json.py
│ │ │ │ └── load_look.py
│ │ │ └── publish/
│ │ │ ├── collect_current_file.py
│ │ │ ├── collect_instance.py
│ │ │ ├── collect_render.py
│ │ │ ├── collect_review.py
│ │ │ ├── collect_workfile.py
│ │ │ ├── extract_abc.py
│ │ │ ├── extract_abc_animation.py
│ │ │ ├── extract_blend.py
│ │ │ ├── extract_blend_animation.py
│ │ │ ├── extract_camera_abc.py
│ │ │ ├── extract_camera_fbx.py
│ │ │ ├── extract_fbx.py
│ │ │ ├── extract_fbx_animation.py
│ │ │ ├── extract_layout.py
│ │ │ ├── extract_playblast.py
│ │ │ ├── extract_thumbnail.py
│ │ │ ├── increment_workfile_version.py
│ │ │ ├── integrate_animation.py
│ │ │ ├── validate_camera_zero_keyframe.py
│ │ │ ├── validate_deadline_publish.py
│ │ │ ├── validate_file_saved.py
│ │ │ ├── validate_instance_empty.py
│ │ │ ├── validate_mesh_has_uv.py
│ │ │ ├── validate_mesh_no_negative_scale.py
│ │ │ ├── validate_no_colons_in_name.py
│ │ │ ├── validate_object_mode.py
│ │ │ ├── validate_render_camera_is_set.py
│ │ │ └── validate_transform_zero.py
│ │ ├── celaction/
│ │ │ ├── __init__.py
│ │ │ ├── addon.py
│ │ │ ├── hooks/
│ │ │ │ └── pre_celaction_setup.py
│ │ │ ├── plugins/
│ │ │ │ ├── __init__.py
│ │ │ │ └── publish/
│ │ │ │ ├── collect_celaction_cli_kwargs.py
│ │ │ │ ├── collect_celaction_instances.py
│ │ │ │ ├── collect_render_path.py
│ │ │ │ └── integrate_version_up.py
│ │ │ ├── resources/
│ │ │ │ └── celaction_template_scene.scn
│ │ │ └── scripts/
│ │ │ ├── __init__.py
│ │ │ └── publish_cli.py
│ │ ├── equalizer/
│ │ │ ├── __init__.py
│ │ │ ├── addon.py
│ │ │ ├── api/
│ │ │ │ ├── __init__.py
│ │ │ │ ├── host.py
│ │ │ │ ├── pipeline.py
│ │ │ │ └── plugin.py
│ │ │ ├── hooks/
│ │ │ │ └── pre_pyside2_install.py
│ │ │ ├── plugins/
│ │ │ │ ├── __init__.py
│ │ │ │ ├── create/
│ │ │ │ │ ├── __init__.py
│ │ │ │ │ ├── create_lens_distortion_data.py
│ │ │ │ │ └── create_matchmove.py
│ │ │ │ ├── load/
│ │ │ │ │ ├── __init__.py
│ │ │ │ │ └── load_plate.py
│ │ │ │ └── publish/
│ │ │ │ ├── __init__.py
│ │ │ │ ├── collect_3de_installation_dir.py
│ │ │ │ ├── collect_camera_data.py
│ │ │ │ ├── collect_workfile.py
│ │ │ │ ├── extract_lens_distortion_nuke.py
│ │ │ │ ├── extract_matchmove_script_maya.py
│ │ │ │ ├── extract_matchmove_script_nuke.py
│ │ │ │ ├── validate_camera_pointgroup.py
│ │ │ │ └── validate_instance_camera_data.py
│ │ │ ├── startup/
│ │ │ │ ├── ayon_create.py
│ │ │ │ ├── ayon_load.py
│ │ │ │ ├── ayon_manage.py
│ │ │ │ ├── ayon_publish.py
│ │ │ │ └── ayon_workfile.py
│ │ │ └── tests/
│ │ │ └── test_plugin.py
│ │ ├── flame/
│ │ │ ├── __init__.py
│ │ │ ├── addon.py
│ │ │ ├── api/
│ │ │ │ ├── __init__.py
│ │ │ │ ├── batch_utils.py
│ │ │ │ ├── constants.py
│ │ │ │ ├── lib.py
│ │ │ │ ├── menu.py
│ │ │ │ ├── pipeline.py
│ │ │ │ ├── plugin.py
│ │ │ │ ├── render_utils.py
│ │ │ │ ├── scripts/
│ │ │ │ │ └── wiretap_com.py
│ │ │ │ ├── utils.py
│ │ │ │ └── workio.py
│ │ │ ├── hooks/
│ │ │ │ └── pre_flame_setup.py
│ │ │ ├── otio/
│ │ │ │ ├── __init__.py
│ │ │ │ ├── flame_export.py
│ │ │ │ └── utils.py
│ │ │ ├── plugins/
│ │ │ │ ├── create/
│ │ │ │ │ └── create_shot_clip.py
│ │ │ │ ├── load/
│ │ │ │ │ ├── load_clip.py
│ │ │ │ │ └── load_clip_batch.py
│ │ │ │ └── publish/
│ │ │ │ ├── collect_test_selection.py
│ │ │ │ ├── collect_timeline_instances.py
│ │ │ │ ├── collect_timeline_otio.py
│ │ │ │ ├── extract_otio_file.py
│ │ │ │ ├── extract_subset_resources.py
│ │ │ │ └── integrate_batch_group.py
│ │ │ └── startup/
│ │ │ ├── openpype_babypublisher/
│ │ │ │ ├── export_preset/
│ │ │ │ │ ├── openpype_seg_thumbnails_jpg.xml
│ │ │ │ │ └── openpype_seg_video_h264.xml
│ │ │ │ ├── modules/
│ │ │ │ │ ├── __init__.py
│ │ │ │ │ ├── app_utils.py
│ │ │ │ │ ├── ftrack_lib.py
│ │ │ │ │ ├── panel_app.py
│ │ │ │ │ └── uiwidgets.py
│ │ │ │ └── openpype_babypublisher.py
│ │ │ └── openpype_in_flame.py
│ │ ├── fusion/
│ │ │ ├── __init__.py
│ │ │ ├── addon.py
│ │ │ ├── api/
│ │ │ │ ├── __init__.py
│ │ │ │ ├── action.py
│ │ │ │ ├── lib.py
│ │ │ │ ├── menu.py
│ │ │ │ ├── pipeline.py
│ │ │ │ ├── plugin.py
│ │ │ │ └── pulse.py
│ │ │ ├── deploy/
│ │ │ │ ├── MenuScripts/
│ │ │ │ │ ├── README.md
│ │ │ │ │ ├── install_pyside2.py
│ │ │ │ │ └── launch_menu.py
│ │ │ │ ├── ayon/
│ │ │ │ │ ├── Config/
│ │ │ │ │ │ └── menu.fu
│ │ │ │ │ └── fusion_shared.prefs
│ │ │ │ └── openpype/
│ │ │ │ ├── Config/
│ │ │ │ │ └── menu.fu
│ │ │ │ └── fusion_shared.prefs
│ │ │ ├── hooks/
│ │ │ │ ├── pre_fusion_profile_hook.py
│ │ │ │ ├── pre_fusion_setup.py
│ │ │ │ └── pre_pyside_install.py
│ │ │ ├── plugins/
│ │ │ │ ├── create/
│ │ │ │ │ ├── create_image_saver.py
│ │ │ │ │ ├── create_saver.py
│ │ │ │ │ └── create_workfile.py
│ │ │ │ ├── inventory/
│ │ │ │ │ ├── select_containers.py
│ │ │ │ │ └── set_tool_color.py
│ │ │ │ ├── load/
│ │ │ │ │ ├── actions.py
│ │ │ │ │ ├── load_alembic.py
│ │ │ │ │ ├── load_fbx.py
│ │ │ │ │ ├── load_sequence.py
│ │ │ │ │ ├── load_usd.py
│ │ │ │ │ └── load_workfile.py
│ │ │ │ └── publish/
│ │ │ │ ├── collect_comp.py
│ │ │ │ ├── collect_comp_frame_range.py
│ │ │ │ ├── collect_inputs.py
│ │ │ │ ├── collect_instances.py
│ │ │ │ ├── collect_render.py
│ │ │ │ ├── collect_workfile.py
│ │ │ │ ├── extract_render_local.py
│ │ │ │ ├── increment_current_file.py
│ │ │ │ ├── save_scene.py
│ │ │ │ ├── validate_background_depth.py
│ │ │ │ ├── validate_comp_saved.py
│ │ │ │ ├── validate_create_folder_checked.py
│ │ │ │ ├── validate_expected_frames_existence.py
│ │ │ │ ├── validate_filename_has_extension.py
│ │ │ │ ├── validate_image_frame.py
│ │ │ │ ├── validate_instance_frame_range.py
│ │ │ │ ├── validate_saver_has_input.py
│ │ │ │ ├── validate_saver_passthrough.py
│ │ │ │ ├── validate_saver_resolution.py
│ │ │ │ └── validate_unique_subsets.py
│ │ │ ├── scripts/
│ │ │ │ ├── __init__.py
│ │ │ │ └── duplicate_with_inputs.py
│ │ │ └── vendor/
│ │ │ ├── attr/
│ │ │ │ ├── __init__.py
│ │ │ │ ├── __init__.pyi
│ │ │ │ ├── _cmp.py
│ │ │ │ ├── _cmp.pyi
│ │ │ │ ├── _compat.py
│ │ │ │ ├── _config.py
│ │ │ │ ├── _funcs.py
│ │ │ │ ├── _make.py
│ │ │ │ ├── _next_gen.py
│ │ │ │ ├── _version_info.py
│ │ │ │ ├── _version_info.pyi
│ │ │ │ ├── converters.py
│ │ │ │ ├── converters.pyi
│ │ │ │ ├── exceptions.py
│ │ │ │ ├── exceptions.pyi
│ │ │ │ ├── filters.py
│ │ │ │ ├── filters.pyi
│ │ │ │ ├── py.typed
│ │ │ │ ├── setters.py
│ │ │ │ ├── setters.pyi
│ │ │ │ ├── validators.py
│ │ │ │ └── validators.pyi
│ │ │ └── urllib3/
│ │ │ ├── __init__.py
│ │ │ ├── _collections.py
│ │ │ ├── _version.py
│ │ │ ├── connection.py
│ │ │ ├── connectionpool.py
│ │ │ ├── contrib/
│ │ │ │ ├── __init__.py
│ │ │ │ ├── _appengine_environ.py
│ │ │ │ ├── _securetransport/
│ │ │ │ │ ├── __init__.py
│ │ │ │ │ ├── bindings.py
│ │ │ │ │ └── low_level.py
│ │ │ │ ├── appengine.py
│ │ │ │ ├── ntlmpool.py
│ │ │ │ ├── pyopenssl.py
│ │ │ │ ├── securetransport.py
│ │ │ │ └── socks.py
│ │ │ ├── exceptions.py
│ │ │ ├── fields.py
│ │ │ ├── filepost.py
│ │ │ ├── packages/
│ │ │ │ ├── __init__.py
│ │ │ │ ├── backports/
│ │ │ │ │ ├── __init__.py
│ │ │ │ │ └── makefile.py
│ │ │ │ ├── six.py
│ │ │ │ └── ssl_match_hostname/
│ │ │ │ ├── __init__.py
│ │ │ │ └── _implementation.py
│ │ │ ├── poolmanager.py
│ │ │ ├── request.py
│ │ │ ├── response.py
│ │ │ └── util/
│ │ │ ├── __init__.py
│ │ │ ├── connection.py
│ │ │ ├── proxy.py
│ │ │ ├── queue.py
│ │ │ ├── request.py
│ │ │ ├── response.py
│ │ │ ├── retry.py
│ │ │ ├── ssl_.py
│ │ │ ├── ssltransport.py
│ │ │ ├── timeout.py
│ │ │ ├── url.py
│ │ │ └── wait.py
│ │ ├── harmony/
│ │ │ ├── __init__.py
│ │ │ ├── addon.py
│ │ │ ├── api/
│ │ │ │ ├── README.md
│ │ │ │ ├── TB_sceneOpened.js
│ │ │ │ ├── __init__.py
│ │ │ │ ├── js/
│ │ │ │ │ ├── .eslintrc.json
│ │ │ │ │ └── AvalonHarmony.js
│ │ │ │ ├── lib.py
│ │ │ │ ├── pipeline.py
│ │ │ │ ├── plugin.py
│ │ │ │ ├── server.py
│ │ │ │ └── workio.py
│ │ │ ├── js/
│ │ │ │ ├── .eslintrc.json
│ │ │ │ ├── PypeHarmony.js
│ │ │ │ ├── README.md
│ │ │ │ ├── creators/
│ │ │ │ │ └── CreateRender.js
│ │ │ │ ├── loaders/
│ │ │ │ │ ├── ImageSequenceLoader.js
│ │ │ │ │ └── TemplateLoader.js
│ │ │ │ └── publish/
│ │ │ │ ├── CollectCurrentFile.js
│ │ │ │ ├── CollectFarmRender.js
│ │ │ │ ├── CollectPalettes.js
│ │ │ │ ├── ExtractPalette.js
│ │ │ │ └── ExtractTemplate.js
│ │ │ ├── plugins/
│ │ │ │ ├── __init__.py
│ │ │ │ ├── create/
│ │ │ │ │ ├── create_farm_render.py
│ │ │ │ │ ├── create_render.py
│ │ │ │ │ └── create_template.py
│ │ │ │ ├── load/
│ │ │ │ │ ├── load_audio.py
│ │ │ │ │ ├── load_background.py
│ │ │ │ │ ├── load_imagesequence.py
│ │ │ │ │ ├── load_palette.py
│ │ │ │ │ ├── load_template.py
│ │ │ │ │ └── load_template_workfile.py
│ │ │ │ └── publish/
│ │ │ │ ├── collect_audio.py
│ │ │ │ ├── collect_current_file.py
│ │ │ │ ├── collect_farm_render.py
│ │ │ │ ├── collect_instances.py
│ │ │ │ ├── collect_palettes.py
│ │ │ │ ├── collect_scene.py
│ │ │ │ ├── collect_workfile.py
│ │ │ │ ├── extract_palette.py
│ │ │ │ ├── extract_render.py
│ │ │ │ ├── extract_save_scene.py
│ │ │ │ ├── extract_template.py
│ │ │ │ ├── extract_workfile.py
│ │ │ │ ├── help/
│ │ │ │ │ ├── validate_audio.xml
│ │ │ │ │ ├── validate_instances.xml
│ │ │ │ │ └── validate_scene_settings.xml
│ │ │ │ ├── increment_workfile.py
│ │ │ │ ├── validate_audio.py
│ │ │ │ ├── validate_instances.py
│ │ │ │ └── validate_scene_settings.py
│ │ │ └── vendor/
│ │ │ ├── .eslintrc.json
│ │ │ └── OpenHarmony/
│ │ │ ├── .gitattributes
│ │ │ ├── .gitignore
│ │ │ ├── Install.bat
│ │ │ ├── LICENSE
│ │ │ ├── README.md
│ │ │ ├── build_doc.bat
│ │ │ ├── documentation.json
│ │ │ ├── install.sh
│ │ │ ├── openHarmony/
│ │ │ │ ├── openHarmony_actions.js
│ │ │ │ ├── openHarmony_application.js
│ │ │ │ ├── openHarmony_attribute.js
│ │ │ │ ├── openHarmony_backdrop.js
│ │ │ │ ├── openHarmony_color.js
│ │ │ │ ├── openHarmony_column.js
│ │ │ │ ├── openHarmony_database.js
│ │ │ │ ├── openHarmony_dialog.js
│ │ │ │ ├── openHarmony_drawing.js
│ │ │ │ ├── openHarmony_element.js
│ │ │ │ ├── openHarmony_file.js
│ │ │ │ ├── openHarmony_frame.js
│ │ │ │ ├── openHarmony_list.js
│ │ │ │ ├── openHarmony_math.js
│ │ │ │ ├── openHarmony_metadata.js
│ │ │ │ ├── openHarmony_misc.js
│ │ │ │ ├── openHarmony_network.js
│ │ │ │ ├── openHarmony_node.js
│ │ │ │ ├── openHarmony_nodeAttributes.js
│ │ │ │ ├── openHarmony_nodeLink.js
│ │ │ │ ├── openHarmony_palette.js
│ │ │ │ ├── openHarmony_path.js
│ │ │ │ ├── openHarmony_preferencedoc.js
│ │ │ │ ├── openHarmony_preferences.js
│ │ │ │ ├── openHarmony_scene.js
│ │ │ │ ├── openHarmony_threading.js
│ │ │ │ ├── openHarmony_timeline.js
│ │ │ │ ├── openHarmony_tool.js
│ │ │ │ └── openHarmony_toolInstall.ui
│ │ │ ├── openHarmony.js
│ │ │ ├── openHarmony_install.js
│ │ │ ├── openHarmony_tools.js
│ │ │ ├── reference/
│ │ │ │ └── Reference_view_currentToolManager().txt
│ │ │ ├── tbpackage.json
│ │ │ └── tools/
│ │ │ └── OpenHarmony_basic/
│ │ │ ├── INSTALL
│ │ │ ├── README
│ │ │ ├── openHarmony_anim_tools.js
│ │ │ ├── openHarmony_basic_backdropPicker.ui
│ │ │ └── openHarmony_rigging_tools.js
│ │ ├── hiero/
│ │ │ ├── __init__.py
│ │ │ ├── addon.py
│ │ │ ├── api/
│ │ │ │ ├── __init__.py
│ │ │ │ ├── constants.py
│ │ │ │ ├── events.py
│ │ │ │ ├── launchforhiero.py
│ │ │ │ ├── lib.py
│ │ │ │ ├── menu.py
│ │ │ │ ├── otio/
│ │ │ │ │ ├── __init__.py
│ │ │ │ │ ├── hiero_export.py
│ │ │ │ │ ├── hiero_import.py
│ │ │ │ │ └── utils.py
│ │ │ │ ├── pipeline.py
│ │ │ │ ├── plugin.py
│ │ │ │ ├── startup/
│ │ │ │ │ ├── HieroPlayer/
│ │ │ │ │ │ └── PlayerPresets.hrox
│ │ │ │ │ ├── Icons/
│ │ │ │ │ │ ├── layers.psd
│ │ │ │ │ │ ├── resolution.psd
│ │ │ │ │ │ ├── retiming.psd
│ │ │ │ │ │ └── review.psd
│ │ │ │ │ ├── Python/
│ │ │ │ │ │ ├── Startup/
│ │ │ │ │ │ │ ├── SpreadsheetExport.py
│ │ │ │ │ │ │ ├── Startup.py
│ │ │ │ │ │ │ ├── otioexporter/
│ │ │ │ │ │ │ │ ├── OTIOExportTask.py
│ │ │ │ │ │ │ │ ├── OTIOExportUI.py
│ │ │ │ │ │ │ │ └── __init__.py
│ │ │ │ │ │ │ ├── project_helpers.py
│ │ │ │ │ │ │ ├── selection_tracker.py
│ │ │ │ │ │ │ └── setFrameRate.py
│ │ │ │ │ │ └── StartupUI/
│ │ │ │ │ │ ├── PimpMySpreadsheet.py
│ │ │ │ │ │ ├── Purge.py
│ │ │ │ │ │ ├── nukeStyleKeyboardShortcuts.py
│ │ │ │ │ │ ├── otioimporter/
│ │ │ │ │ │ │ ├── OTIOImport.py
│ │ │ │ │ │ │ └── __init__.py
│ │ │ │ │ │ └── setPosterFrame.py
│ │ │ │ │ └── TaskPresets/
│ │ │ │ │ ├── 10.5/
│ │ │ │ │ │ └── Processors/
│ │ │ │ │ │ └── hiero.exporters.FnShotProcessor.ShotProcessor/
│ │ │ │ │ │ └── pipeline.xml
│ │ │ │ │ ├── 11.1/
│ │ │ │ │ │ └── Processors/
│ │ │ │ │ │ └── hiero.exporters.FnShotProcessor.ShotProcessor/
│ │ │ │ │ │ └── pipeline.xml
│ │ │ │ │ └── 11.2/
│ │ │ │ │ └── hiero.exporters.FnShotProcessor.ShotProcessor/
│ │ │ │ │ └── pipeline.xml
│ │ │ │ ├── style.css
│ │ │ │ ├── tags.py
│ │ │ │ └── workio.py
│ │ │ ├── plugins/
│ │ │ │ ├── create/
│ │ │ │ │ └── create_shot_clip.py
│ │ │ │ ├── load/
│ │ │ │ │ ├── load_clip.py
│ │ │ │ │ └── load_effects.py
│ │ │ │ ├── publish/
│ │ │ │ │ ├── collect_clip_effects.py
│ │ │ │ │ ├── collect_frame_tag_instances.py
│ │ │ │ │ ├── collect_tag_tasks.py
│ │ │ │ │ ├── extract_clip_effects.py
│ │ │ │ │ ├── extract_frames.py
│ │ │ │ │ ├── extract_thumbnail.py
│ │ │ │ │ ├── integrate_version_up_workfile.py
│ │ │ │ │ ├── precollect_instances.py
│ │ │ │ │ └── precollect_workfile.py
│ │ │ │ └── publish_old_workflow/
│ │ │ │ ├── collect_assetbuilds.py
│ │ │ │ ├── collect_tag_comments.py
│ │ │ │ └── precollect_retime.py
│ │ │ └── vendor/
│ │ │ └── google/
│ │ │ └── protobuf/
│ │ │ ├── __init__.py
│ │ │ ├── any_pb2.py
│ │ │ ├── api_pb2.py
│ │ │ ├── compiler/
│ │ │ │ ├── __init__.py
│ │ │ │ └── plugin_pb2.py
│ │ │ ├── descriptor.py
│ │ │ ├── descriptor_database.py
│ │ │ ├── descriptor_pb2.py
│ │ │ ├── descriptor_pool.py
│ │ │ ├── duration_pb2.py
│ │ │ ├── empty_pb2.py
│ │ │ ├── field_mask_pb2.py
│ │ │ ├── internal/
│ │ │ │ ├── __init__.py
│ │ │ │ ├── _parameterized.py
│ │ │ │ ├── api_implementation.py
│ │ │ │ ├── builder.py
│ │ │ │ ├── containers.py
│ │ │ │ ├── decoder.py
│ │ │ │ ├── encoder.py
│ │ │ │ ├── enum_type_wrapper.py
│ │ │ │ ├── extension_dict.py
│ │ │ │ ├── message_listener.py
│ │ │ │ ├── message_set_extensions_pb2.py
│ │ │ │ ├── missing_enum_values_pb2.py
│ │ │ │ ├── more_extensions_dynamic_pb2.py
│ │ │ │ ├── more_extensions_pb2.py
│ │ │ │ ├── more_messages_pb2.py
│ │ │ │ ├── no_package_pb2.py
│ │ │ │ ├── python_message.py
│ │ │ │ ├── type_checkers.py
│ │ │ │ ├── well_known_types.py
│ │ │ │ └── wire_format.py
│ │ │ ├── json_format.py
│ │ │ ├── message.py
│ │ │ ├── message_factory.py
│ │ │ ├── proto_builder.py
│ │ │ ├── pyext/
│ │ │ │ ├── __init__.py
│ │ │ │ ├── cpp_message.py
│ │ │ │ └── python_pb2.py
│ │ │ ├── reflection.py
│ │ │ ├── service.py
│ │ │ ├── service_reflection.py
│ │ │ ├── source_context_pb2.py
│ │ │ ├── struct_pb2.py
│ │ │ ├── symbol_database.py
│ │ │ ├── text_encoding.py
│ │ │ ├── text_format.py
│ │ │ ├── timestamp_pb2.py
│ │ │ ├── type_pb2.py
│ │ │ ├── util/
│ │ │ │ ├── __init__.py
│ │ │ │ ├── json_format_pb2.py
│ │ │ │ └── json_format_proto3_pb2.py
│ │ │ └── wrappers_pb2.py
│ │ ├── houdini/
│ │ │ ├── __init__.py
│ │ │ ├── addon.py
│ │ │ ├── api/
│ │ │ │ ├── __init__.py
│ │ │ │ ├── action.py
│ │ │ │ ├── colorspace.py
│ │ │ │ ├── creator_node_shelves.py
│ │ │ │ ├── lib.py
│ │ │ │ ├── pipeline.py
│ │ │ │ ├── plugin.py
│ │ │ │ ├── shelves.py
│ │ │ │ └── usd.py
│ │ │ ├── hooks/
│ │ │ │ └── set_paths.py
│ │ │ ├── plugins/
│ │ │ │ ├── create/
│ │ │ │ │ ├── convert_legacy.py
│ │ │ │ │ ├── create_alembic_camera.py
│ │ │ │ │ ├── create_arnold_ass.py
│ │ │ │ │ ├── create_arnold_rop.py
│ │ │ │ │ ├── create_bgeo.py
│ │ │ │ │ ├── create_composite.py
│ │ │ │ │ ├── create_hda.py
│ │ │ │ │ ├── create_karma_rop.py
│ │ │ │ │ ├── create_mantra_ifd.py
│ │ │ │ │ ├── create_mantra_rop.py
│ │ │ │ │ ├── create_pointcache.py
│ │ │ │ │ ├── create_redshift_proxy.py
│ │ │ │ │ ├── create_redshift_rop.py
│ │ │ │ │ ├── create_review.py
│ │ │ │ │ ├── create_staticmesh.py
│ │ │ │ │ ├── create_usd.py
│ │ │ │ │ ├── create_usdrender.py
│ │ │ │ │ ├── create_vbd_cache.py
│ │ │ │ │ ├── create_vray_rop.py
│ │ │ │ │ └── create_workfile.py
│ │ │ │ ├── inventory/
│ │ │ │ │ └── set_camera_resolution.py
│ │ │ │ ├── load/
│ │ │ │ │ ├── actions.py
│ │ │ │ │ ├── load_alembic.py
│ │ │ │ │ ├── load_alembic_archive.py
│ │ │ │ │ ├── load_ass.py
│ │ │ │ │ ├── load_bgeo.py
│ │ │ │ │ ├── load_camera.py
│ │ │ │ │ ├── load_fbx.py
│ │ │ │ │ ├── load_hda.py
│ │ │ │ │ ├── load_image.py
│ │ │ │ │ ├── load_redshift_proxy.py
│ │ │ │ │ ├── load_usd_layer.py
│ │ │ │ │ ├── load_usd_reference.py
│ │ │ │ │ ├── load_vdb.py
│ │ │ │ │ └── show_usdview.py
│ │ │ │ └── publish/
│ │ │ │ ├── collect_active_state.py
│ │ │ │ ├── collect_arnold_rop.py
│ │ │ │ ├── collect_asset_handles.py
│ │ │ │ ├── collect_cache_farm.py
│ │ │ │ ├── collect_chunk_size.py
│ │ │ │ ├── collect_current_file.py
│ │ │ │ ├── collect_frames.py
│ │ │ │ ├── collect_inputs.py
│ │ │ │ ├── collect_instances.py
│ │ │ │ ├── collect_instances_usd_layered.py
│ │ │ │ ├── collect_karma_rop.py
│ │ │ │ ├── collect_mantra_rop.py
│ │ │ │ ├── collect_output_node.py
│ │ │ │ ├── collect_pointcache_type.py
│ │ │ │ ├── collect_redshift_rop.py
│ │ │ │ ├── collect_remote_publish.py
│ │ │ │ ├── collect_render_products.py
│ │ │ │ ├── collect_review_data.py
│ │ │ │ ├── collect_rop_frame_range.py
│ │ │ │ ├── collect_staticmesh_type.py
│ │ │ │ ├── collect_usd_bootstrap.py
│ │ │ │ ├── collect_usd_layers.py
│ │ │ │ ├── collect_vray_rop.py
│ │ │ │ ├── collect_workfile.py
│ │ │ │ ├── collect_workscene_fps.py
│ │ │ │ ├── extract_alembic.py
│ │ │ │ ├── extract_ass.py
│ │ │ │ ├── extract_bgeo.py
│ │ │ │ ├── extract_composite.py
│ │ │ │ ├── extract_fbx.py
│ │ │ │ ├── extract_hda.py
│ │ │ │ ├── extract_mantra_ifd.py
│ │ │ │ ├── extract_opengl.py
│ │ │ │ ├── extract_redshift_proxy.py
│ │ │ │ ├── extract_usd.py
│ │ │ │ ├── extract_usd_layered.py
│ │ │ │ ├── extract_vdb_cache.py
│ │ │ │ ├── help/
│ │ │ │ │ └── validate_vdb_output_node.xml
│ │ │ │ ├── increment_current_file.py
│ │ │ │ ├── save_scene.py
│ │ │ │ ├── validate_abc_primitive_to_detail.py
│ │ │ │ ├── validate_alembic_face_sets.py
│ │ │ │ ├── validate_alembic_input_node.py
│ │ │ │ ├── validate_animation_settings.py
│ │ │ │ ├── validate_bypass.py
│ │ │ │ ├── validate_camera_rop.py
│ │ │ │ ├── validate_cop_output_node.py
│ │ │ │ ├── validate_fbx_output_node.py
│ │ │ │ ├── validate_file_extension.py
│ │ │ │ ├── validate_frame_range.py
│ │ │ │ ├── validate_frame_token.py
│ │ │ │ ├── validate_houdini_license_category.py
│ │ │ │ ├── validate_mesh_is_static.py
│ │ │ │ ├── validate_mkpaths_toggled.py
│ │ │ │ ├── validate_no_errors.py
│ │ │ │ ├── validate_primitive_hierarchy_paths.py
│ │ │ │ ├── validate_remote_publish.py
│ │ │ │ ├── validate_remote_publish_enabled.py
│ │ │ │ ├── validate_review_colorspace.py
│ │ │ │ ├── validate_scene_review.py
│ │ │ │ ├── validate_sop_output_node.py
│ │ │ │ ├── validate_subset_name.py
│ │ │ │ ├── validate_unreal_staticmesh_naming.py
│ │ │ │ ├── validate_usd_layer_path_backslashes.py
│ │ │ │ ├── validate_usd_model_and_shade.py
│ │ │ │ ├── validate_usd_output_node.py
│ │ │ │ ├── validate_usd_render_product_names.py
│ │ │ │ ├── validate_usd_setdress.py
│ │ │ │ ├── validate_usd_shade_model_exists.py
│ │ │ │ ├── validate_usd_shade_workspace.py
│ │ │ │ ├── validate_vdb_output_node.py
│ │ │ │ └── validate_workfile_paths.py
│ │ │ └── startup/
│ │ │ ├── MainMenuCommon.xml
│ │ │ ├── python2.7libs/
│ │ │ │ └── pythonrc.py
│ │ │ ├── python3.10libs/
│ │ │ │ └── pythonrc.py
│ │ │ ├── python3.7libs/
│ │ │ │ └── pythonrc.py
│ │ │ └── python3.9libs/
│ │ │ └── pythonrc.py
│ │ ├── max/
│ │ │ ├── __init__.py
│ │ │ ├── addon.py
│ │ │ ├── api/
│ │ │ │ ├── __init__.py
│ │ │ │ ├── action.py
│ │ │ │ ├── colorspace.py
│ │ │ │ ├── lib.py
│ │ │ │ ├── lib_renderproducts.py
│ │ │ │ ├── lib_rendersettings.py
│ │ │ │ ├── menu.py
│ │ │ │ ├── pipeline.py
│ │ │ │ ├── plugin.py
│ │ │ │ └── preview_animation.py
│ │ │ ├── hooks/
│ │ │ │ ├── force_startup_script.py
│ │ │ │ ├── inject_python.py
│ │ │ │ └── set_paths.py
│ │ │ ├── plugins/
│ │ │ │ ├── __init__.py
│ │ │ │ ├── create/
│ │ │ │ │ ├── create_camera.py
│ │ │ │ │ ├── create_maxScene.py
│ │ │ │ │ ├── create_model.py
│ │ │ │ │ ├── create_pointcache.py
│ │ │ │ │ ├── create_pointcloud.py
│ │ │ │ │ ├── create_redshift_proxy.py
│ │ │ │ │ ├── create_render.py
│ │ │ │ │ ├── create_review.py
│ │ │ │ │ ├── create_tycache.py
│ │ │ │ │ └── create_workfile.py
│ │ │ │ ├── load/
│ │ │ │ │ ├── load_camera_fbx.py
│ │ │ │ │ ├── load_max_scene.py
│ │ │ │ │ ├── load_model.py
│ │ │ │ │ ├── load_model_fbx.py
│ │ │ │ │ ├── load_model_obj.py
│ │ │ │ │ ├── load_model_usd.py
│ │ │ │ │ ├── load_pointcache.py
│ │ │ │ │ ├── load_pointcache_ornatrix.py
│ │ │ │ │ ├── load_pointcloud.py
│ │ │ │ │ ├── load_redshift_proxy.py
│ │ │ │ │ └── load_tycache.py
│ │ │ │ └── publish/
│ │ │ │ ├── collect_frame_range.py
│ │ │ │ ├── collect_members.py
│ │ │ │ ├── collect_render.py
│ │ │ │ ├── collect_review.py
│ │ │ │ ├── collect_tycache_attributes.py
│ │ │ │ ├── collect_workfile.py
│ │ │ │ ├── extract_alembic.py
│ │ │ │ ├── extract_fbx.py
│ │ │ │ ├── extract_max_scene_raw.py
│ │ │ │ ├── extract_model_obj.py
│ │ │ │ ├── extract_model_usd.py
│ │ │ │ ├── extract_pointcloud.py
│ │ │ │ ├── extract_redshift_proxy.py
│ │ │ │ ├── extract_review_animation.py
│ │ │ │ ├── extract_thumbnail.py
│ │ │ │ ├── extract_tycache.py
│ │ │ │ ├── increment_workfile_version.py
│ │ │ │ ├── save_scene.py
│ │ │ │ ├── save_scenes_for_cameras.py
│ │ │ │ ├── validate_attributes.py
│ │ │ │ ├── validate_camera_attributes.py
│ │ │ │ ├── validate_camera_contents.py
│ │ │ │ ├── validate_frame_range.py
│ │ │ │ ├── validate_instance_has_members.py
│ │ │ │ ├── validate_instance_in_context.py
│ │ │ │ ├── validate_loaded_plugin.py
│ │ │ │ ├── validate_model_contents.py
│ │ │ │ ├── validate_pointcloud.py
│ │ │ │ ├── validate_renderable_camera.py
│ │ │ │ ├── validate_renderer_redshift_proxy.py
│ │ │ │ ├── validate_renderpasses.py
│ │ │ │ ├── validate_resolution_setting.py
│ │ │ │ ├── validate_scene_saved.py
│ │ │ │ └── validate_tyflow_data.py
│ │ │ └── startup/
│ │ │ ├── startup.ms
│ │ │ └── startup.py
│ │ ├── maya/
│ │ │ ├── __init__.py
│ │ │ ├── addon.py
│ │ │ ├── api/
│ │ │ │ ├── __init__.py
│ │ │ │ ├── action.py
│ │ │ │ ├── alembic.py
│ │ │ │ ├── commands.py
│ │ │ │ ├── customize.py
│ │ │ │ ├── exitstack.py
│ │ │ │ ├── fbx.py
│ │ │ │ ├── gltf.py
│ │ │ │ ├── lib.py
│ │ │ │ ├── lib_renderproducts.py
│ │ │ │ ├── lib_rendersettings.py
│ │ │ │ ├── lib_rendersetup.py
│ │ │ │ ├── menu.py
│ │ │ │ ├── pipeline.py
│ │ │ │ ├── plugin.py
│ │ │ │ ├── render_setup_tools.py
│ │ │ │ ├── setdress.py
│ │ │ │ ├── shader_definition_editor.py
│ │ │ │ ├── workfile_template_builder.py
│ │ │ │ └── workio.py
│ │ │ ├── hooks/
│ │ │ │ ├── pre_auto_load_plugins.py
│ │ │ │ ├── pre_copy_mel.py
│ │ │ │ └── pre_open_workfile_post_initialization.py
│ │ │ ├── lib.py
│ │ │ ├── plugins/
│ │ │ │ ├── __init__.py
│ │ │ │ ├── create/
│ │ │ │ │ ├── convert_legacy.py
│ │ │ │ │ ├── create_animation_pointcache.py
│ │ │ │ │ ├── create_arnold_scene_source.py
│ │ │ │ │ ├── create_assembly.py
│ │ │ │ │ ├── create_camera.py
│ │ │ │ │ ├── create_layout.py
│ │ │ │ │ ├── create_look.py
│ │ │ │ │ ├── create_matchmove.py
│ │ │ │ │ ├── create_maya_usd.py
│ │ │ │ │ ├── create_mayascene.py
│ │ │ │ │ ├── create_model.py
│ │ │ │ │ ├── create_multishot_layout.py
│ │ │ │ │ ├── create_multiverse_look.py
│ │ │ │ │ ├── create_multiverse_usd.py
│ │ │ │ │ ├── create_multiverse_usd_comp.py
│ │ │ │ │ ├── create_multiverse_usd_over.py
│ │ │ │ │ ├── create_proxy_abc.py
│ │ │ │ │ ├── create_redshift_proxy.py
│ │ │ │ │ ├── create_render.py
│ │ │ │ │ ├── create_rendersetup.py
│ │ │ │ │ ├── create_review.py
│ │ │ │ │ ├── create_rig.py
│ │ │ │ │ ├── create_setdress.py
│ │ │ │ │ ├── create_unreal_skeletalmesh.py
│ │ │ │ │ ├── create_unreal_staticmesh.py
│ │ │ │ │ ├── create_unreal_yeticache.py
│ │ │ │ │ ├── create_vrayproxy.py
│ │ │ │ │ ├── create_vrayscene.py
│ │ │ │ │ ├── create_workfile.py
│ │ │ │ │ ├── create_xgen.py
│ │ │ │ │ ├── create_yeti_cache.py
│ │ │ │ │ └── create_yeti_rig.py
│ │ │ │ ├── inventory/
│ │ │ │ │ ├── connect_geometry.py
│ │ │ │ │ ├── connect_xgen.py
│ │ │ │ │ ├── connect_yeti_rig.py
│ │ │ │ │ ├── import_modelrender.py
│ │ │ │ │ ├── import_reference.py
│ │ │ │ │ ├── rig_recreate_animation_instance.py
│ │ │ │ │ └── select_containers.py
│ │ │ │ ├── load/
│ │ │ │ │ ├── _load_animation.py
│ │ │ │ │ ├── actions.py
│ │ │ │ │ ├── load_arnold_standin.py
│ │ │ │ │ ├── load_assembly.py
│ │ │ │ │ ├── load_audio.py
│ │ │ │ │ ├── load_gpucache.py
│ │ │ │ │ ├── load_image.py
│ │ │ │ │ ├── load_image_plane.py
│ │ │ │ │ ├── load_look.py
│ │ │ │ │ ├── load_matchmove.py
│ │ │ │ │ ├── load_maya_usd.py
│ │ │ │ │ ├── load_multiverse_usd.py
│ │ │ │ │ ├── load_multiverse_usd_over.py
│ │ │ │ │ ├── load_redshift_proxy.py
│ │ │ │ │ ├── load_reference.py
│ │ │ │ │ ├── load_rendersetup.py
│ │ │ │ │ ├── load_vdb_to_arnold.py
│ │ │ │ │ ├── load_vdb_to_redshift.py
│ │ │ │ │ ├── load_vdb_to_vray.py
│ │ │ │ │ ├── load_vrayproxy.py
│ │ │ │ │ ├── load_vrayscene.py
│ │ │ │ │ ├── load_xgen.py
│ │ │ │ │ ├── load_yeti_cache.py
│ │ │ │ │ └── load_yeti_rig.py
│ │ │ │ └── publish/
│ │ │ │ ├── __init__.py
│ │ │ │ ├── collect_animation.py
│ │ │ │ ├── collect_arnold_scene_source.py
│ │ │ │ ├── collect_assembly.py
│ │ │ │ ├── collect_current_file.py
│ │ │ │ ├── collect_fbx_animation.py
│ │ │ │ ├── collect_fbx_camera.py
│ │ │ │ ├── collect_file_dependencies.py
│ │ │ │ ├── collect_gltf.py
│ │ │ │ ├── collect_history.py
│ │ │ │ ├── collect_inputs.py
│ │ │ │ ├── collect_instances.py
│ │ │ │ ├── collect_look.py
│ │ │ │ ├── collect_maya_scene_time.py
│ │ │ │ ├── collect_maya_units.py
│ │ │ │ ├── collect_maya_workspace.py
│ │ │ │ ├── collect_model.py
│ │ │ │ ├── collect_multiverse_look.py
│ │ │ │ ├── collect_pointcache.py
│ │ │ │ ├── collect_remove_marked.py
│ │ │ │ ├── collect_render.py
│ │ │ │ ├── collect_render_layer_aovs.py
│ │ │ │ ├── collect_renderable_camera.py
│ │ │ │ ├── collect_review.py
│ │ │ │ ├── collect_rig_sets.py
│ │ │ │ ├── collect_skeleton_mesh.py
│ │ │ │ ├── collect_unreal_skeletalmesh.py
│ │ │ │ ├── collect_unreal_staticmesh.py
│ │ │ │ ├── collect_user_defined_attributes.py
│ │ │ │ ├── collect_vrayproxy.py
│ │ │ │ ├── collect_vrayscene.py
│ │ │ │ ├── collect_workfile.py
│ │ │ │ ├── collect_workscene_fps.py
│ │ │ │ ├── collect_xgen.py
│ │ │ │ ├── collect_yeti_cache.py
│ │ │ │ ├── collect_yeti_rig.py
│ │ │ │ ├── determine_future_version.py
│ │ │ │ ├── extract_active_view_thumbnail.py
│ │ │ │ ├── extract_arnold_scene_source.py
│ │ │ │ ├── extract_assembly.py
│ │ │ │ ├── extract_camera_alembic.py
│ │ │ │ ├── extract_camera_mayaScene.py
│ │ │ │ ├── extract_fbx.py
│ │ │ │ ├── extract_fbx_animation.py
│ │ │ │ ├── extract_gltf.py
│ │ │ │ ├── extract_gpu_cache.py
│ │ │ │ ├── extract_import_reference.py
│ │ │ │ ├── extract_layout.py
│ │ │ │ ├── extract_look.py
│ │ │ │ ├── extract_maya_scene_raw.py
│ │ │ │ ├── extract_maya_usd.py
│ │ │ │ ├── extract_model.py
│ │ │ │ ├── extract_multiverse_look.py
│ │ │ │ ├── extract_multiverse_usd.py
│ │ │ │ ├── extract_multiverse_usd_comp.py
│ │ │ │ ├── extract_multiverse_usd_over.py
│ │ │ │ ├── extract_obj.py
│ │ │ │ ├── extract_playblast.py
│ │ │ │ ├── extract_pointcache.py
│ │ │ │ ├── extract_proxy_abc.py
│ │ │ │ ├── extract_redshift_proxy.py
│ │ │ │ ├── extract_rendersetup.py
│ │ │ │ ├── extract_rig.py
│ │ │ │ ├── extract_skeleton_mesh.py
│ │ │ │ ├── extract_thumbnail.py
│ │ │ │ ├── extract_unreal_skeletalmesh_abc.py
│ │ │ │ ├── extract_unreal_skeletalmesh_fbx.py
│ │ │ │ ├── extract_unreal_staticmesh.py
│ │ │ │ ├── extract_unreal_yeticache.py
│ │ │ │ ├── extract_vrayproxy.py
│ │ │ │ ├── extract_vrayscene.py
│ │ │ │ ├── extract_workfile_xgen.py
│ │ │ │ ├── extract_xgen.py
│ │ │ │ ├── extract_yeti_cache.py
│ │ │ │ ├── extract_yeti_rig.py
│ │ │ │ ├── help/
│ │ │ │ │ ├── submit_maya_remote_publish_deadline.xml
│ │ │ │ │ ├── validate_maya_units.xml
│ │ │ │ │ ├── validate_node_ids.xml
│ │ │ │ │ └── validate_skeletalmesh_hierarchy.xml
│ │ │ │ ├── increment_current_file_deadline.py
│ │ │ │ ├── reset_xgen_attributes.py
│ │ │ │ ├── save_scene.py
│ │ │ │ ├── validate_alembic_options_defaults.py
│ │ │ │ ├── validate_animated_reference.py
│ │ │ │ ├── validate_animation_content.py
│ │ │ │ ├── validate_animation_out_set_related_node_ids.py
│ │ │ │ ├── validate_arnold_scene_source.py
│ │ │ │ ├── validate_arnold_scene_source_cbid.py
│ │ │ │ ├── validate_ass_relative_paths.py
│ │ │ │ ├── validate_assembly_name.py
│ │ │ │ ├── validate_assembly_namespaces.py
│ │ │ │ ├── validate_assembly_transforms.py
│ │ │ │ ├── validate_attributes.py
│ │ │ │ ├── validate_camera_attributes.py
│ │ │ │ ├── validate_camera_contents.py
│ │ │ │ ├── validate_color_sets.py
│ │ │ │ ├── validate_current_renderlayer_renderable.py
│ │ │ │ ├── validate_cycle_error.py
│ │ │ │ ├── validate_frame_range.py
│ │ │ │ ├── validate_glsl_material.py
│ │ │ │ ├── validate_glsl_plugin.py
│ │ │ │ ├── validate_instance_has_members.py
│ │ │ │ ├── validate_instance_in_context.py
│ │ │ │ ├── validate_instance_subset.py
│ │ │ │ ├── validate_instancer_content.py
│ │ │ │ ├── validate_instancer_frame_ranges.py
│ │ │ │ ├── validate_loaded_plugin.py
│ │ │ │ ├── validate_look_contents.py
│ │ │ │ ├── validate_look_default_shaders_connections.py
│ │ │ │ ├── validate_look_id_reference_edits.py
│ │ │ │ ├── validate_look_no_default_shaders.py
│ │ │ │ ├── validate_look_sets.py
│ │ │ │ ├── validate_look_shading_group.py
│ │ │ │ ├── validate_look_single_shader.py
│ │ │ │ ├── validate_maya_units.py
│ │ │ │ ├── validate_mesh_arnold_attributes.py
│ │ │ │ ├── validate_mesh_empty.py
│ │ │ │ ├── validate_mesh_has_uv.py
│ │ │ │ ├── validate_mesh_lamina_faces.py
│ │ │ │ ├── validate_mesh_ngons.py
│ │ │ │ ├── validate_mesh_no_negative_scale.py
│ │ │ │ ├── validate_mesh_non_manifold.py
│ │ │ │ ├── validate_mesh_non_zero_edge.py
│ │ │ │ ├── validate_mesh_normals_unlocked.py
│ │ │ │ ├── validate_mesh_overlapping_uvs.py
│ │ │ │ ├── validate_mesh_shader_connections.py
│ │ │ │ ├── validate_mesh_single_uv_set.py
│ │ │ │ ├── validate_mesh_uv_set_map1.py
│ │ │ │ ├── validate_mesh_vertices_have_edges.py
│ │ │ │ ├── validate_model_content.py
│ │ │ │ ├── validate_model_name.py
│ │ │ │ ├── validate_mvlook_contents.py
│ │ │ │ ├── validate_no_animation.py
│ │ │ │ ├── validate_no_default_camera.py
│ │ │ │ ├── validate_no_namespace.py
│ │ │ │ ├── validate_no_null_transforms.py
│ │ │ │ ├── validate_no_unknown_nodes.py
│ │ │ │ ├── validate_no_vraymesh.py
│ │ │ │ ├── validate_node_ids.py
│ │ │ │ ├── validate_node_ids_deformed_shapes.py
│ │ │ │ ├── validate_node_ids_in_database.py
│ │ │ │ ├── validate_node_ids_related.py
│ │ │ │ ├── validate_node_ids_unique.py
│ │ │ │ ├── validate_node_no_ghosting.py
│ │ │ │ ├── validate_plugin_path_attributes.py
│ │ │ │ ├── validate_render_image_rule.py
│ │ │ │ ├── validate_render_no_default_cameras.py
│ │ │ │ ├── validate_render_single_camera.py
│ │ │ │ ├── validate_renderlayer_aovs.py
│ │ │ │ ├── validate_rendersettings.py
│ │ │ │ ├── validate_resolution.py
│ │ │ │ ├── validate_resources.py
│ │ │ │ ├── validate_review.py
│ │ │ │ ├── validate_rig_contents.py
│ │ │ │ ├── validate_rig_controllers.py
│ │ │ │ ├── validate_rig_controllers_arnold_attributes.py
│ │ │ │ ├── validate_rig_joints_hidden.py
│ │ │ │ ├── validate_rig_out_set_node_ids.py
│ │ │ │ ├── validate_rig_output_ids.py
│ │ │ │ ├── validate_scene_set_workspace.py
│ │ │ │ ├── validate_setdress_root.py
│ │ │ │ ├── validate_shader_name.py
│ │ │ │ ├── validate_shape_default_names.py
│ │ │ │ ├── validate_shape_render_stats.py
│ │ │ │ ├── validate_shape_zero.py
│ │ │ │ ├── validate_single_assembly.py
│ │ │ │ ├── validate_skeletalmesh_hierarchy.py
│ │ │ │ ├── validate_skeletalmesh_triangulated.py
│ │ │ │ ├── validate_skeleton_top_group_hierarchy.py
│ │ │ │ ├── validate_skinCluster_deformer_set.py
│ │ │ │ ├── validate_step_size.py
│ │ │ │ ├── validate_transform_naming_suffix.py
│ │ │ │ ├── validate_transform_zero.py
│ │ │ │ ├── validate_unique_names.py
│ │ │ │ ├── validate_unreal_mesh_triangulated.py
│ │ │ │ ├── validate_unreal_staticmesh_naming.py
│ │ │ │ ├── validate_unreal_up_axis.py
│ │ │ │ ├── validate_visible_only.py
│ │ │ │ ├── validate_vray.py
│ │ │ │ ├── validate_vray_distributed_rendering.py
│ │ │ │ ├── validate_vray_referenced_aovs.py
│ │ │ │ ├── validate_vray_translator_settings.py
│ │ │ │ ├── validate_vrayproxy.py
│ │ │ │ ├── validate_vrayproxy_members.py
│ │ │ │ ├── validate_xgen.py
│ │ │ │ ├── validate_yeti_renderscript_callbacks.py
│ │ │ │ ├── validate_yeti_rig_cache_state.py
│ │ │ │ ├── validate_yeti_rig_input_in_instance.py
│ │ │ │ └── validate_yeti_rig_settings.py
│ │ │ ├── startup/
│ │ │ │ └── userSetup.py
│ │ │ └── tools/
│ │ │ ├── __init__.py
│ │ │ └── mayalookassigner/
│ │ │ ├── LICENSE
│ │ │ ├── __init__.py
│ │ │ ├── alembic.py
│ │ │ ├── app.py
│ │ │ ├── arnold_standin.py
│ │ │ ├── commands.py
│ │ │ ├── lib.py
│ │ │ ├── models.py
│ │ │ ├── usd.py
│ │ │ ├── views.py
│ │ │ ├── vray_proxies.py
│ │ │ └── widgets.py
│ │ ├── nuke/
│ │ │ ├── __init__.py
│ │ │ ├── addon.py
│ │ │ ├── api/
│ │ │ │ ├── __init__.py
│ │ │ │ ├── actions.py
│ │ │ │ ├── command.py
│ │ │ │ ├── constants.py
│ │ │ │ ├── gizmo_menu.py
│ │ │ │ ├── lib.py
│ │ │ │ ├── pipeline.py
│ │ │ │ ├── plugin.py
│ │ │ │ ├── push_to_project.py
│ │ │ │ ├── utils.py
│ │ │ │ ├── workfile_template_builder.py
│ │ │ │ └── workio.py
│ │ │ ├── hooks/
│ │ │ │ └── pre_nukeassist_setup.py
│ │ │ ├── plugins/
│ │ │ │ ├── __init__.py
│ │ │ │ ├── create/
│ │ │ │ │ ├── __init__.py
│ │ │ │ │ ├── convert_legacy.py
│ │ │ │ │ ├── create_backdrop.py
│ │ │ │ │ ├── create_camera.py
│ │ │ │ │ ├── create_gizmo.py
│ │ │ │ │ ├── create_model.py
│ │ │ │ │ ├── create_source.py
│ │ │ │ │ ├── create_write_image.py
│ │ │ │ │ ├── create_write_prerender.py
│ │ │ │ │ ├── create_write_render.py
│ │ │ │ │ └── workfile_creator.py
│ │ │ │ ├── inventory/
│ │ │ │ │ ├── repair_old_loaders.py
│ │ │ │ │ └── select_containers.py
│ │ │ │ ├── load/
│ │ │ │ │ ├── actions.py
│ │ │ │ │ ├── load_backdrop.py
│ │ │ │ │ ├── load_camera_abc.py
│ │ │ │ │ ├── load_clip.py
│ │ │ │ │ ├── load_effects.py
│ │ │ │ │ ├── load_effects_ip.py
│ │ │ │ │ ├── load_gizmo.py
│ │ │ │ │ ├── load_gizmo_ip.py
│ │ │ │ │ ├── load_image.py
│ │ │ │ │ ├── load_matchmove.py
│ │ │ │ │ ├── load_model.py
│ │ │ │ │ ├── load_ociolook.py
│ │ │ │ │ └── load_script_precomp.py
│ │ │ │ └── publish/
│ │ │ │ ├── collect_backdrop.py
│ │ │ │ ├── collect_context_data.py
│ │ │ │ ├── collect_framerate.py
│ │ │ │ ├── collect_gizmo.py
│ │ │ │ ├── collect_model.py
│ │ │ │ ├── collect_nuke_instance_data.py
│ │ │ │ ├── collect_reads.py
│ │ │ │ ├── collect_slate_node.py
│ │ │ │ ├── collect_workfile.py
│ │ │ │ ├── collect_writes.py
│ │ │ │ ├── extract_backdrop.py
│ │ │ │ ├── extract_camera.py
│ │ │ │ ├── extract_gizmo.py
│ │ │ │ ├── extract_model.py
│ │ │ │ ├── extract_ouput_node.py
│ │ │ │ ├── extract_output_directory.py
│ │ │ │ ├── extract_render_local.py
│ │ │ │ ├── extract_review_data.py
│ │ │ │ ├── extract_review_data_lut.py
│ │ │ │ ├── extract_review_intermediates.py
│ │ │ │ ├── extract_script_save.py
│ │ │ │ ├── extract_slate_frame.py
│ │ │ │ ├── help/
│ │ │ │ │ ├── validate_asset_context.xml
│ │ │ │ │ ├── validate_backdrop.xml
│ │ │ │ │ ├── validate_gizmo.xml
│ │ │ │ │ ├── validate_knobs.xml
│ │ │ │ │ ├── validate_output_resolution.xml
│ │ │ │ │ ├── validate_proxy_mode.xml
│ │ │ │ │ ├── validate_rendered_frames.xml
│ │ │ │ │ ├── validate_script_attributes.xml
│ │ │ │ │ └── validate_write_nodes.xml
│ │ │ │ ├── increment_script_version.py
│ │ │ │ ├── remove_ouput_node.py
│ │ │ │ ├── validate_asset_context.py
│ │ │ │ ├── validate_backdrop.py
│ │ │ │ ├── validate_exposed_knobs.py
│ │ │ │ ├── validate_gizmo.py
│ │ │ │ ├── validate_knobs.py
│ │ │ │ ├── validate_output_resolution.py
│ │ │ │ ├── validate_proxy_mode.py
│ │ │ │ ├── validate_rendered_frames.py
│ │ │ │ ├── validate_script_attributes.py
│ │ │ │ └── validate_write_nodes.py
│ │ │ ├── startup/
│ │ │ │ ├── __init__.py
│ │ │ │ ├── clear_rendered.py
│ │ │ │ ├── custom_write_node.py
│ │ │ │ ├── frame_setting_for_read_nodes.py
│ │ │ │ ├── menu.py
│ │ │ │ └── write_to_read.py
│ │ │ └── vendor/
│ │ │ └── google/
│ │ │ └── protobuf/
│ │ │ ├── __init__.py
│ │ │ ├── any_pb2.py
│ │ │ ├── api_pb2.py
│ │ │ ├── compiler/
│ │ │ │ ├── __init__.py
│ │ │ │ └── plugin_pb2.py
│ │ │ ├── descriptor.py
│ │ │ ├── descriptor_database.py
│ │ │ ├── descriptor_pb2.py
│ │ │ ├── descriptor_pool.py
│ │ │ ├── duration_pb2.py
│ │ │ ├── empty_pb2.py
│ │ │ ├── field_mask_pb2.py
│ │ │ ├── internal/
│ │ │ │ ├── __init__.py
│ │ │ │ ├── _parameterized.py
│ │ │ │ ├── api_implementation.py
│ │ │ │ ├── builder.py
│ │ │ │ ├── containers.py
│ │ │ │ ├── decoder.py
│ │ │ │ ├── encoder.py
│ │ │ │ ├── enum_type_wrapper.py
│ │ │ │ ├── extension_dict.py
│ │ │ │ ├── message_listener.py
│ │ │ │ ├── message_set_extensions_pb2.py
│ │ │ │ ├── missing_enum_values_pb2.py
│ │ │ │ ├── more_extensions_dynamic_pb2.py
│ │ │ │ ├── more_extensions_pb2.py
│ │ │ │ ├── more_messages_pb2.py
│ │ │ │ ├── no_package_pb2.py
│ │ │ │ ├── python_message.py
│ │ │ │ ├── type_checkers.py
│ │ │ │ ├── well_known_types.py
│ │ │ │ └── wire_format.py
│ │ │ ├── json_format.py
│ │ │ ├── message.py
│ │ │ ├── message_factory.py
│ │ │ ├── proto_builder.py
│ │ │ ├── pyext/
│ │ │ │ ├── __init__.py
│ │ │ │ ├── cpp_message.py
│ │ │ │ └── python_pb2.py
│ │ │ ├── reflection.py
│ │ │ ├── service.py
│ │ │ ├── service_reflection.py
│ │ │ ├── source_context_pb2.py
│ │ │ ├── struct_pb2.py
│ │ │ ├── symbol_database.py
│ │ │ ├── text_encoding.py
│ │ │ ├── text_format.py
│ │ │ ├── timestamp_pb2.py
│ │ │ ├── type_pb2.py
│ │ │ ├── util/
│ │ │ │ ├── __init__.py
│ │ │ │ ├── json_format_pb2.py
│ │ │ │ └── json_format_proto3_pb2.py
│ │ │ └── wrappers_pb2.py
│ │ ├── photoshop/
│ │ │ ├── __init__.py
│ │ │ ├── addon.py
│ │ │ ├── api/
│ │ │ │ ├── README.md
│ │ │ │ ├── __init__.py
│ │ │ │ ├── extension/
│ │ │ │ │ ├── .debug
│ │ │ │ │ ├── CSXS/
│ │ │ │ │ │ └── manifest.xml
│ │ │ │ │ ├── client/
│ │ │ │ │ │ ├── CSInterface.js
│ │ │ │ │ │ ├── client.js
│ │ │ │ │ │ └── wsrpc.js
│ │ │ │ │ ├── host/
│ │ │ │ │ │ ├── JSX.js
│ │ │ │ │ │ ├── index.jsx
│ │ │ │ │ │ └── json.js
│ │ │ │ │ └── index.html
│ │ │ │ ├── extension.zxp
│ │ │ │ ├── launch_logic.py
│ │ │ │ ├── lib.py
│ │ │ │ ├── pipeline.py
│ │ │ │ ├── plugin.py
│ │ │ │ └── ws_stub.py
│ │ │ ├── lib.py
│ │ │ ├── plugins/
│ │ │ │ ├── create/
│ │ │ │ │ ├── create_flatten_image.py
│ │ │ │ │ ├── create_image.py
│ │ │ │ │ ├── create_review.py
│ │ │ │ │ └── create_workfile.py
│ │ │ │ ├── load/
│ │ │ │ │ ├── load_image.py
│ │ │ │ │ ├── load_image_from_sequence.py
│ │ │ │ │ └── load_reference.py
│ │ │ │ └── publish/
│ │ │ │ ├── closePS.py
│ │ │ │ ├── collect_auto_image.py
│ │ │ │ ├── collect_auto_image_refresh.py
│ │ │ │ ├── collect_auto_review.py
│ │ │ │ ├── collect_auto_workfile.py
│ │ │ │ ├── collect_batch_data.py
│ │ │ │ ├── collect_color_coded_instances.py
│ │ │ │ ├── collect_current_file.py
│ │ │ │ ├── collect_extension_version.py
│ │ │ │ ├── collect_image.py
│ │ │ │ ├── collect_published_version.py
│ │ │ │ ├── collect_review.py
│ │ │ │ ├── collect_version.py
│ │ │ │ ├── collect_workfile.py
│ │ │ │ ├── extract_image.py
│ │ │ │ ├── extract_review.py
│ │ │ │ ├── extract_save_scene.py
│ │ │ │ ├── help/
│ │ │ │ │ ├── validate_instance_asset.xml
│ │ │ │ │ └── validate_naming.xml
│ │ │ │ ├── increment_workfile.py
│ │ │ │ ├── validate_instance_asset.py
│ │ │ │ └── validate_naming.py
│ │ │ └── resources/
│ │ │ └── template.psd
│ │ ├── resolve/
│ │ │ ├── README.markdown
│ │ │ ├── RESOLVE_API_v18.5.1-build6.txt
│ │ │ ├── __init__.py
│ │ │ ├── addon.py
│ │ │ ├── api/
│ │ │ │ ├── __init__.py
│ │ │ │ ├── action.py
│ │ │ │ ├── lib.py
│ │ │ │ ├── menu.py
│ │ │ │ ├── menu_style.qss
│ │ │ │ ├── pipeline.py
│ │ │ │ ├── plugin.py
│ │ │ │ ├── testing_utils.py
│ │ │ │ ├── todo-rendering.py
│ │ │ │ ├── utils.py
│ │ │ │ └── workio.py
│ │ │ ├── hooks/
│ │ │ │ ├── pre_resolve_last_workfile.py
│ │ │ │ ├── pre_resolve_setup.py
│ │ │ │ └── pre_resolve_startup.py
│ │ │ ├── otio/
│ │ │ │ ├── __init__.py
│ │ │ │ ├── davinci_export.py
│ │ │ │ ├── davinci_import.py
│ │ │ │ └── utils.py
│ │ │ ├── plugins/
│ │ │ │ ├── create/
│ │ │ │ │ └── create_shot_clip.py
│ │ │ │ ├── load/
│ │ │ │ │ └── load_clip.py
│ │ │ │ └── publish/
│ │ │ │ ├── extract_workfile.py
│ │ │ │ ├── precollect_instances.py
│ │ │ │ └── precollect_workfile.py
│ │ │ ├── startup.py
│ │ │ ├── utility_scripts/
│ │ │ │ ├── AYON__Menu.py
│ │ │ │ ├── OpenPype__Menu.py
│ │ │ │ ├── develop/
│ │ │ │ │ ├── OTIO_export.py
│ │ │ │ │ ├── OTIO_import.py
│ │ │ │ │ └── OpenPype_sync_util_scripts.py
│ │ │ │ └── openpype_startup.scriptlib
│ │ │ └── utils.py
│ │ ├── standalonepublisher/
│ │ │ ├── __init__.py
│ │ │ ├── addon.py
│ │ │ └── plugins/
│ │ │ └── publish/
│ │ │ ├── collect_app_name.py
│ │ │ ├── collect_bulk_mov_instances.py
│ │ │ ├── collect_context.py
│ │ │ ├── collect_editorial.py
│ │ │ ├── collect_editorial_instances.py
│ │ │ ├── collect_editorial_resources.py
│ │ │ ├── collect_harmony_scenes.py
│ │ │ ├── collect_harmony_zips.py
│ │ │ ├── collect_hierarchy.py
│ │ │ ├── collect_instance_data.py
│ │ │ ├── collect_matching_asset.py
│ │ │ ├── collect_remove_marked.py
│ │ │ ├── collect_representation_names.py
│ │ │ ├── collect_texture.py
│ │ │ ├── extract_resources.py
│ │ │ ├── extract_thumbnail.py
│ │ │ ├── extract_workfile_location.py
│ │ │ ├── help/
│ │ │ │ ├── validate_editorial_resources.xml
│ │ │ │ ├── validate_frame_ranges.xml
│ │ │ │ ├── validate_shot_duplicates.xml
│ │ │ │ ├── validate_simple_texture_naming.xml
│ │ │ │ ├── validate_sources.xml
│ │ │ │ ├── validate_task_existence.xml
│ │ │ │ ├── validate_texture_batch.xml
│ │ │ │ ├── validate_texture_has_workfile.xml
│ │ │ │ ├── validate_texture_name.xml
│ │ │ │ ├── validate_texture_versions.xml
│ │ │ │ └── validate_texture_workfiles.xml
│ │ │ ├── validate_editorial_resources.py
│ │ │ ├── validate_frame_ranges.py
│ │ │ ├── validate_shot_duplicates.py
│ │ │ ├── validate_sources.py
│ │ │ ├── validate_task_existence.py
│ │ │ ├── validate_texture_batch.py
│ │ │ ├── validate_texture_has_workfile.py
│ │ │ ├── validate_texture_name.py
│ │ │ ├── validate_texture_versions.py
│ │ │ └── validate_texture_workfiles.py
│ │ ├── substancepainter/
│ │ │ ├── __init__.py
│ │ │ ├── addon.py
│ │ │ ├── api/
│ │ │ │ ├── __init__.py
│ │ │ │ ├── colorspace.py
│ │ │ │ ├── lib.py
│ │ │ │ └── pipeline.py
│ │ │ ├── deploy/
│ │ │ │ ├── plugins/
│ │ │ │ │ └── openpype_plugin.py
│ │ │ │ └── startup/
│ │ │ │ └── openpype_load_on_first_run.py
│ │ │ └── plugins/
│ │ │ ├── create/
│ │ │ │ ├── create_textures.py
│ │ │ │ └── create_workfile.py
│ │ │ ├── load/
│ │ │ │ └── load_mesh.py
│ │ │ └── publish/
│ │ │ ├── collect_current_file.py
│ │ │ ├── collect_textureset_images.py
│ │ │ ├── collect_workfile_representation.py
│ │ │ ├── extract_textures.py
│ │ │ ├── increment_workfile.py
│ │ │ ├── save_workfile.py
│ │ │ └── validate_ouput_maps.py
│ │ ├── traypublisher/
│ │ │ ├── __init__.py
│ │ │ ├── addon.py
│ │ │ ├── api/
│ │ │ │ ├── __init__.py
│ │ │ │ ├── editorial.py
│ │ │ │ ├── pipeline.py
│ │ │ │ └── plugin.py
│ │ │ ├── batch_parsing.py
│ │ │ └── plugins/
│ │ │ ├── create/
│ │ │ │ ├── create_colorspace_look.py
│ │ │ │ ├── create_editorial.py
│ │ │ │ ├── create_from_settings.py
│ │ │ │ ├── create_movie_batch.py
│ │ │ │ └── create_online.py
│ │ │ └── publish/
│ │ │ ├── collect_app_name.py
│ │ │ ├── collect_clip_instances.py
│ │ │ ├── collect_colorspace_look.py
│ │ │ ├── collect_editorial_instances.py
│ │ │ ├── collect_editorial_reviewable.py
│ │ │ ├── collect_explicit_colorspace.py
│ │ │ ├── collect_frame_data_from_asset_entity.py
│ │ │ ├── collect_movie_batch.py
│ │ │ ├── collect_online_file.py
│ │ │ ├── collect_review_frames.py
│ │ │ ├── collect_sequence_frame_data.py
│ │ │ ├── collect_shot_instances.py
│ │ │ ├── collect_simple_instances.py
│ │ │ ├── collect_source.py
│ │ │ ├── extract_colorspace_look.py
│ │ │ ├── help/
│ │ │ │ ├── validate_existing_version.xml
│ │ │ │ └── validate_frame_ranges.xml
│ │ │ ├── validate_colorspace.py
│ │ │ ├── validate_colorspace_look.py
│ │ │ ├── validate_existing_version.py
│ │ │ ├── validate_filepaths.py
│ │ │ ├── validate_frame_ranges.py
│ │ │ └── validate_online_file.py
│ │ ├── tvpaint/
│ │ │ ├── __init__.py
│ │ │ ├── addon.py
│ │ │ ├── api/
│ │ │ │ ├── __init__.py
│ │ │ │ ├── communication_server.py
│ │ │ │ ├── launch_script.py
│ │ │ │ ├── lib.py
│ │ │ │ ├── pipeline.py
│ │ │ │ └── plugin.py
│ │ │ ├── hooks/
│ │ │ │ └── pre_launch_args.py
│ │ │ ├── lib.py
│ │ │ ├── plugins/
│ │ │ │ ├── create/
│ │ │ │ │ ├── convert_legacy.py
│ │ │ │ │ ├── create_render.py
│ │ │ │ │ ├── create_review.py
│ │ │ │ │ └── create_workfile.py
│ │ │ │ ├── load/
│ │ │ │ │ ├── load_image.py
│ │ │ │ │ ├── load_reference_image.py
│ │ │ │ │ ├── load_sound.py
│ │ │ │ │ └── load_workfile.py
│ │ │ │ └── publish/
│ │ │ │ ├── collect_instance_frames.py
│ │ │ │ ├── collect_render_instances.py
│ │ │ │ ├── collect_workfile.py
│ │ │ │ ├── collect_workfile_data.py
│ │ │ │ ├── extract_convert_to_exr.py
│ │ │ │ ├── extract_sequence.py
│ │ │ │ ├── help/
│ │ │ │ │ ├── validate_asset_name.xml
│ │ │ │ │ ├── validate_duplicated_layer_names.xml
│ │ │ │ │ ├── validate_layers_visibility.xml
│ │ │ │ │ ├── validate_marks.xml
│ │ │ │ │ ├── validate_missing_layer_names.xml
│ │ │ │ │ ├── validate_render_layer_group.xml
│ │ │ │ │ ├── validate_render_pass_group.xml
│ │ │ │ │ ├── validate_scene_settings.xml
│ │ │ │ │ ├── validate_start_frame.xml
│ │ │ │ │ ├── validate_workfile_metadata.xml
│ │ │ │ │ └── validate_workfile_project_name.xml
│ │ │ │ ├── increment_workfile_version.py
│ │ │ │ ├── validate_asset_name.py
│ │ │ │ ├── validate_duplicated_layer_names.py
│ │ │ │ ├── validate_layers_visibility.py
│ │ │ │ ├── validate_marks.py
│ │ │ │ ├── validate_missing_layer_names.py
│ │ │ │ ├── validate_render_layer_group.py
│ │ │ │ ├── validate_render_pass_group.py
│ │ │ │ ├── validate_scene_settings.py
│ │ │ │ ├── validate_start_frame.py
│ │ │ │ ├── validate_workfile_metadata.py
│ │ │ │ └── validate_workfile_project_name.py
│ │ │ ├── resources/
│ │ │ │ └── template.tvpp
│ │ │ ├── tvpaint_plugin/
│ │ │ │ ├── __init__.py
│ │ │ │ └── plugin_code/
│ │ │ │ ├── CMakeLists.txt
│ │ │ │ ├── README.md
│ │ │ │ ├── library.cpp
│ │ │ │ └── library.def
│ │ │ └── worker/
│ │ │ ├── __init__.py
│ │ │ ├── init_file.tvpp
│ │ │ ├── worker.py
│ │ │ └── worker_job.py
│ │ ├── unreal/
│ │ │ ├── README.md
│ │ │ ├── __init__.py
│ │ │ ├── addon.py
│ │ │ ├── api/
│ │ │ │ ├── __init__.py
│ │ │ │ ├── helpers.py
│ │ │ │ ├── pipeline.py
│ │ │ │ ├── plugin.py
│ │ │ │ ├── rendering.py
│ │ │ │ └── tools_ui.py
│ │ │ ├── hooks/
│ │ │ │ └── pre_workfile_preparation.py
│ │ │ ├── lib.py
│ │ │ ├── plugins/
│ │ │ │ ├── __init__.py
│ │ │ │ ├── create/
│ │ │ │ │ ├── create_camera.py
│ │ │ │ │ ├── create_layout.py
│ │ │ │ │ ├── create_look.py
│ │ │ │ │ ├── create_render.py
│ │ │ │ │ ├── create_staticmeshfbx.py
│ │ │ │ │ └── create_uasset.py
│ │ │ │ ├── inventory/
│ │ │ │ │ ├── delete_unused_assets.py
│ │ │ │ │ └── update_actors.py
│ │ │ │ ├── load/
│ │ │ │ │ ├── load_alembic_animation.py
│ │ │ │ │ ├── load_animation.py
│ │ │ │ │ ├── load_camera.py
│ │ │ │ │ ├── load_geometrycache_abc.py
│ │ │ │ │ ├── load_layout.py
│ │ │ │ │ ├── load_layout_existing.py
│ │ │ │ │ ├── load_skeletalmesh_abc.py
│ │ │ │ │ ├── load_skeletalmesh_fbx.py
│ │ │ │ │ ├── load_staticmesh_abc.py
│ │ │ │ │ ├── load_staticmesh_fbx.py
│ │ │ │ │ ├── load_uasset.py
│ │ │ │ │ └── load_yeticache.py
│ │ │ │ └── publish/
│ │ │ │ ├── collect_current_file.py
│ │ │ │ ├── collect_instance_members.py
│ │ │ │ ├── collect_remove_marked.py
│ │ │ │ ├── collect_render_instances.py
│ │ │ │ ├── extract_camera.py
│ │ │ │ ├── extract_layout.py
│ │ │ │ ├── extract_look.py
│ │ │ │ ├── extract_uasset.py
│ │ │ │ ├── validate_no_dependencies.py
│ │ │ │ └── validate_sequence_frames.py
│ │ │ ├── ue_workers.py
│ │ │ └── ui/
│ │ │ ├── __init__.py
│ │ │ └── splash_screen.py
│ │ └── webpublisher/
│ │ ├── README.md
│ │ ├── __init__.py
│ │ ├── addon.py
│ │ ├── api/
│ │ │ └── __init__.py
│ │ ├── lib.py
│ │ ├── plugins/
│ │ │ └── publish/
│ │ │ ├── collect_batch_data.py
│ │ │ ├── collect_fps.py
│ │ │ ├── collect_published_files.py
│ │ │ ├── collect_tvpaint_instances.py
│ │ │ ├── collect_tvpaint_workfile_data.py
│ │ │ ├── extract_tvpaint_workfile.py
│ │ │ ├── others_cleanup_job_root.py
│ │ │ └── validate_tvpaint_workfile_data.py
│ │ ├── publish_functions.py
│ │ └── webserver_service/
│ │ ├── __init__.py
│ │ ├── webpublish_routes.py
│ │ └── webserver.py
│ ├── lib/
│ │ ├── __init__.py
│ │ ├── applications.py
│ │ ├── attribute_definitions.py
│ │ ├── connections.py
│ │ ├── dateutils.py
│ │ ├── env_tools.py
│ │ ├── events.py
│ │ ├── execute.py
│ │ ├── file_transaction.py
│ │ ├── local_settings.py
│ │ ├── log.py
│ │ ├── openpype_version.py
│ │ ├── path_templates.py
│ │ ├── path_tools.py
│ │ ├── plugin_tools.py
│ │ ├── profiles_filtering.py
│ │ ├── profiling.py
│ │ ├── project_backpack.py
│ │ ├── pype_info.py
│ │ ├── python_2_comp.py
│ │ ├── python_module_tools.py
│ │ ├── terminal.py
│ │ ├── transcoding.py
│ │ ├── usdlib.py
│ │ └── vendor_bin_utils.py
│ ├── modules/
│ │ ├── README.md
│ │ ├── __init__.py
│ │ ├── asset_reporter/
│ │ │ ├── __init__.py
│ │ │ ├── module.py
│ │ │ └── window.py
│ │ ├── avalon_apps/
│ │ │ ├── __init__.py
│ │ │ ├── avalon_app.py
│ │ │ └── rest_api.py
│ │ ├── base.py
│ │ ├── click_wrap.py
│ │ ├── clockify/
│ │ │ ├── __init__.py
│ │ │ ├── clockify_api.py
│ │ │ ├── clockify_module.py
│ │ │ ├── constants.py
│ │ │ ├── ftrack/
│ │ │ │ ├── server/
│ │ │ │ │ └── action_clockify_sync_server.py
│ │ │ │ └── user/
│ │ │ │ └── action_clockify_sync_local.py
│ │ │ ├── launcher_actions/
│ │ │ │ ├── ClockifyStart.py
│ │ │ │ └── ClockifySync.py
│ │ │ └── widgets.py
│ │ ├── deadline/
│ │ │ ├── __init__.py
│ │ │ ├── abstract_submit_deadline.py
│ │ │ ├── deadline_module.py
│ │ │ ├── plugins/
│ │ │ │ └── publish/
│ │ │ │ ├── collect_deadline_server_from_instance.py
│ │ │ │ ├── collect_default_deadline_server.py
│ │ │ │ ├── collect_pools.py
│ │ │ │ ├── collect_publishable_instances.py
│ │ │ │ ├── help/
│ │ │ │ │ └── validate_deadline_pools.xml
│ │ │ │ ├── submit_aftereffects_deadline.py
│ │ │ │ ├── submit_blender_deadline.py
│ │ │ │ ├── submit_celaction_deadline.py
│ │ │ │ ├── submit_fusion_deadline.py
│ │ │ │ ├── submit_harmony_deadline.py
│ │ │ │ ├── submit_houdini_cache_deadline.py
│ │ │ │ ├── submit_houdini_remote_publish.py
│ │ │ │ ├── submit_houdini_render_deadline.py
│ │ │ │ ├── submit_max_deadline.py
│ │ │ │ ├── submit_maya_deadline.py
│ │ │ │ ├── submit_maya_remote_publish_deadline.py
│ │ │ │ ├── submit_nuke_deadline.py
│ │ │ │ ├── submit_publish_cache_job.py
│ │ │ │ ├── submit_publish_job.py
│ │ │ │ ├── validate_deadline_connection.py
│ │ │ │ ├── validate_deadline_pools.py
│ │ │ │ └── validate_expected_and_rendered_files.py
│ │ │ └── repository/
│ │ │ ├── custom/
│ │ │ │ └── plugins/
│ │ │ │ ├── Ayon/
│ │ │ │ │ ├── Ayon.options
│ │ │ │ │ ├── Ayon.param
│ │ │ │ │ └── Ayon.py
│ │ │ │ ├── CelAction/
│ │ │ │ │ ├── CelAction.param
│ │ │ │ │ └── CelAction.py
│ │ │ │ ├── GlobalJobPreLoad.py
│ │ │ │ ├── HarmonyOpenPype/
│ │ │ │ │ ├── HarmonyOpenPype.options
│ │ │ │ │ ├── HarmonyOpenPype.param
│ │ │ │ │ └── HarmonyOpenPype.py
│ │ │ │ ├── OpenPype/
│ │ │ │ │ ├── OpenPype.options
│ │ │ │ │ ├── OpenPype.param
│ │ │ │ │ └── OpenPype.py
│ │ │ │ └── OpenPypeTileAssembler/
│ │ │ │ ├── OpenPypeTileAssembler.options
│ │ │ │ ├── OpenPypeTileAssembler.param
│ │ │ │ └── OpenPypeTileAssembler.py
│ │ │ └── readme.md
│ │ ├── example_addons/
│ │ │ ├── example_addon/
│ │ │ │ ├── __init__.py
│ │ │ │ ├── addon.py
│ │ │ │ ├── plugins/
│ │ │ │ │ └── publish/
│ │ │ │ │ └── example_plugin.py
│ │ │ │ ├── settings/
│ │ │ │ │ ├── defaults/
│ │ │ │ │ │ ├── project_settings.json
│ │ │ │ │ │ └── system_settings.json
│ │ │ │ │ ├── dynamic_schemas/
│ │ │ │ │ │ ├── project_dynamic_schemas.json
│ │ │ │ │ │ └── system_dynamic_schemas.json
│ │ │ │ │ └── schemas/
│ │ │ │ │ ├── project_schemas/
│ │ │ │ │ │ ├── main.json
│ │ │ │ │ │ └── the_template.json
│ │ │ │ │ └── system_schemas/
│ │ │ │ │ └── main.json
│ │ │ │ └── widgets.py
│ │ │ └── tiny_addon.py
│ │ ├── ftrack/
│ │ │ ├── __init__.py
│ │ │ ├── event_handlers_server/
│ │ │ │ ├── action_clone_review_session.py
│ │ │ │ ├── action_create_review_session.py
│ │ │ │ ├── action_multiple_notes.py
│ │ │ │ ├── action_prepare_project.py
│ │ │ │ ├── action_private_project_detection.py
│ │ │ │ ├── action_push_frame_values_to_task.py
│ │ │ │ ├── action_sync_to_avalon.py
│ │ │ │ ├── action_tranfer_hierarchical_values.py
│ │ │ │ ├── event_del_avalon_id_from_new.py
│ │ │ │ ├── event_first_version_status.py
│ │ │ │ ├── event_next_task_update.py
│ │ │ │ ├── event_push_frame_values_to_task.py
│ │ │ │ ├── event_radio_buttons.py
│ │ │ │ ├── event_sync_links.py
│ │ │ │ ├── event_sync_to_avalon.py
│ │ │ │ ├── event_task_to_parent_status.py
│ │ │ │ ├── event_task_to_version_status.py
│ │ │ │ ├── event_thumbnail_updates.py
│ │ │ │ ├── event_user_assigment.py
│ │ │ │ └── event_version_to_task_statuses.py
│ │ │ ├── event_handlers_user/
│ │ │ │ ├── action_applications.py
│ │ │ │ ├── action_batch_task_creation.py
│ │ │ │ ├── action_clean_hierarchical_attributes.py
│ │ │ │ ├── action_client_review_sort.py
│ │ │ │ ├── action_component_open.py
│ │ │ │ ├── action_create_cust_attrs.py
│ │ │ │ ├── action_create_folders.py
│ │ │ │ ├── action_create_project_structure.py
│ │ │ │ ├── action_delete_asset.py
│ │ │ │ ├── action_delete_old_versions.py
│ │ │ │ ├── action_delivery.py
│ │ │ │ ├── action_djvview.py
│ │ │ │ ├── action_fill_workfile_attr.py
│ │ │ │ ├── action_job_killer.py
│ │ │ │ ├── action_multiple_notes.py
│ │ │ │ ├── action_prepare_project.py
│ │ │ │ ├── action_rv.py
│ │ │ │ ├── action_seed.py
│ │ │ │ ├── action_store_thumbnails_to_avalon.py
│ │ │ │ ├── action_sync_to_avalon.py
│ │ │ │ ├── action_test.py
│ │ │ │ ├── action_thumbnail_to_childern.py
│ │ │ │ ├── action_thumbnail_to_parent.py
│ │ │ │ └── action_where_run_ask.py
│ │ │ ├── ftrack_module.py
│ │ │ ├── ftrack_server/
│ │ │ │ ├── __init__.py
│ │ │ │ ├── event_server_cli.py
│ │ │ │ ├── ftrack_server.py
│ │ │ │ ├── lib.py
│ │ │ │ └── socket_thread.py
│ │ │ ├── launch_hooks/
│ │ │ │ └── post_ftrack_changes.py
│ │ │ ├── lib/
│ │ │ │ ├── __init__.py
│ │ │ │ ├── avalon_sync.py
│ │ │ │ ├── constants.py
│ │ │ │ ├── credentials.py
│ │ │ │ ├── custom_attributes.json
│ │ │ │ ├── custom_attributes.py
│ │ │ │ ├── ftrack_action_handler.py
│ │ │ │ ├── ftrack_base_handler.py
│ │ │ │ ├── ftrack_event_handler.py
│ │ │ │ └── settings.py
│ │ │ ├── plugins/
│ │ │ │ ├── _unused_publish/
│ │ │ │ │ └── integrate_ftrack_comments.py
│ │ │ │ └── publish/
│ │ │ │ ├── collect_custom_attributes_data.py
│ │ │ │ ├── collect_ftrack_api.py
│ │ │ │ ├── collect_ftrack_family.py
│ │ │ │ ├── collect_local_ftrack_creds.py
│ │ │ │ ├── collect_username.py
│ │ │ │ ├── integrate_ftrack_api.py
│ │ │ │ ├── integrate_ftrack_component_overwrite.py
│ │ │ │ ├── integrate_ftrack_description.py
│ │ │ │ ├── integrate_ftrack_instances.py
│ │ │ │ ├── integrate_ftrack_note.py
│ │ │ │ ├── integrate_ftrack_status.py
│ │ │ │ ├── integrate_hierarchy_ftrack.py
│ │ │ │ └── validate_custom_ftrack_attributes.py
│ │ │ ├── python2_vendor/
│ │ │ │ └── ftrack-python-api/
│ │ │ │ ├── .gitignore
│ │ │ │ ├── LICENSE.python
│ │ │ │ ├── LICENSE.txt
│ │ │ │ ├── MANIFEST.in
│ │ │ │ ├── README.rst
│ │ │ │ ├── bitbucket-pipelines.yml
│ │ │ │ ├── doc/
│ │ │ │ │ ├── _static/
│ │ │ │ │ │ └── ftrack.css
│ │ │ │ │ ├── api_reference/
│ │ │ │ │ │ ├── accessor/
│ │ │ │ │ │ │ ├── base.rst
│ │ │ │ │ │ │ ├── disk.rst
│ │ │ │ │ │ │ ├── index.rst
│ │ │ │ │ │ │ └── server.rst
│ │ │ │ │ │ ├── attribute.rst
│ │ │ │ │ │ ├── cache.rst
│ │ │ │ │ │ ├── collection.rst
│ │ │ │ │ │ ├── entity/
│ │ │ │ │ │ │ ├── asset_version.rst
│ │ │ │ │ │ │ ├── base.rst
│ │ │ │ │ │ │ ├── component.rst
│ │ │ │ │ │ │ ├── factory.rst
│ │ │ │ │ │ │ ├── index.rst
│ │ │ │ │ │ │ ├── job.rst
│ │ │ │ │ │ │ ├── location.rst
│ │ │ │ │ │ │ ├── note.rst
│ │ │ │ │ │ │ ├── project_schema.rst
│ │ │ │ │ │ │ └── user.rst
│ │ │ │ │ │ ├── event/
│ │ │ │ │ │ │ ├── base.rst
│ │ │ │ │ │ │ ├── expression.rst
│ │ │ │ │ │ │ ├── hub.rst
│ │ │ │ │ │ │ ├── index.rst
│ │ │ │ │ │ │ ├── subscriber.rst
│ │ │ │ │ │ │ └── subscription.rst
│ │ │ │ │ │ ├── exception.rst
│ │ │ │ │ │ ├── formatter.rst
│ │ │ │ │ │ ├── index.rst
│ │ │ │ │ │ ├── inspection.rst
│ │ │ │ │ │ ├── logging.rst
│ │ │ │ │ │ ├── operation.rst
│ │ │ │ │ │ ├── plugin.rst
│ │ │ │ │ │ ├── query.rst
│ │ │ │ │ │ ├── resource_identifier_transformer/
│ │ │ │ │ │ │ ├── base.rst
│ │ │ │ │ │ │ └── index.rst
│ │ │ │ │ │ ├── session.rst
│ │ │ │ │ │ ├── structure/
│ │ │ │ │ │ │ ├── base.rst
│ │ │ │ │ │ │ ├── id.rst
│ │ │ │ │ │ │ ├── index.rst
│ │ │ │ │ │ │ ├── origin.rst
│ │ │ │ │ │ │ └── standard.rst
│ │ │ │ │ │ └── symbol.rst
│ │ │ │ │ ├── caching.rst
│ │ │ │ │ ├── conf.py
│ │ │ │ │ ├── docutils.conf
│ │ │ │ │ ├── environment_variables.rst
│ │ │ │ │ ├── event_list.rst
│ │ │ │ │ ├── example/
│ │ │ │ │ │ ├── assignments_and_allocations.rst
│ │ │ │ │ │ ├── component.rst
│ │ │ │ │ │ ├── custom_attribute.rst
│ │ │ │ │ │ ├── encode_media.rst
│ │ │ │ │ │ ├── entity_links.rst
│ │ │ │ │ │ ├── index.rst
│ │ │ │ │ │ ├── invite_user.rst
│ │ │ │ │ │ ├── job.rst
│ │ │ │ │ │ ├── link_attribute.rst
│ │ │ │ │ │ ├── list.rst
│ │ │ │ │ │ ├── manage_custom_attribute_configuration.rst
│ │ │ │ │ │ ├── metadata.rst
│ │ │ │ │ │ ├── note.rst
│ │ │ │ │ │ ├── project.rst
│ │ │ │ │ │ ├── publishing.rst
│ │ │ │ │ │ ├── review_session.rst
│ │ │ │ │ │ ├── scope.rst
│ │ │ │ │ │ ├── security_roles.rst
│ │ │ │ │ │ ├── sync_ldap_users.rst
│ │ │ │ │ │ ├── task_template.rst
│ │ │ │ │ │ ├── thumbnail.rst
│ │ │ │ │ │ ├── timer.rst
│ │ │ │ │ │ └── web_review.rst
│ │ │ │ │ ├── glossary.rst
│ │ │ │ │ ├── handling_events.rst
│ │ │ │ │ ├── index.rst
│ │ │ │ │ ├── installing.rst
│ │ │ │ │ ├── introduction.rst
│ │ │ │ │ ├── locations/
│ │ │ │ │ │ ├── configuring.rst
│ │ │ │ │ │ ├── index.rst
│ │ │ │ │ │ ├── overview.rst
│ │ │ │ │ │ └── tutorial.rst
│ │ │ │ │ ├── querying.rst
│ │ │ │ │ ├── release/
│ │ │ │ │ │ ├── index.rst
│ │ │ │ │ │ ├── migrating_from_old_api.rst
│ │ │ │ │ │ ├── migration.rst
│ │ │ │ │ │ └── release_notes.rst
│ │ │ │ │ ├── resource/
│ │ │ │ │ │ ├── example_plugin.py
│ │ │ │ │ │ ├── example_plugin_safe.py
│ │ │ │ │ │ └── example_plugin_using_session.py
│ │ │ │ │ ├── security_and_authentication.rst
│ │ │ │ │ ├── tutorial.rst
│ │ │ │ │ ├── understanding_sessions.rst
│ │ │ │ │ └── working_with_entities.rst
│ │ │ │ ├── pytest.ini
│ │ │ │ ├── resource/
│ │ │ │ │ └── plugin/
│ │ │ │ │ ├── configure_locations.py
│ │ │ │ │ └── construct_entity_type.py
│ │ │ │ ├── setup.cfg
│ │ │ │ ├── setup.py
│ │ │ │ ├── source/
│ │ │ │ │ ├── __init__.py
│ │ │ │ │ └── ftrack_api/
│ │ │ │ │ ├── __init__.py
│ │ │ │ │ ├── _centralized_storage_scenario.py
│ │ │ │ │ ├── _python_ntpath.py
│ │ │ │ │ ├── _version.py
│ │ │ │ │ ├── _weakref.py
│ │ │ │ │ ├── accessor/
│ │ │ │ │ │ ├── __init__.py
│ │ │ │ │ │ ├── base.py
│ │ │ │ │ │ ├── disk.py
│ │ │ │ │ │ └── server.py
│ │ │ │ │ ├── attribute.py
│ │ │ │ │ ├── cache.py
│ │ │ │ │ ├── collection.py
│ │ │ │ │ ├── data.py
│ │ │ │ │ ├── entity/
│ │ │ │ │ │ ├── __init__.py
│ │ │ │ │ │ ├── asset_version.py
│ │ │ │ │ │ ├── base.py
│ │ │ │ │ │ ├── component.py
│ │ │ │ │ │ ├── factory.py
│ │ │ │ │ │ ├── job.py
│ │ │ │ │ │ ├── location.py
│ │ │ │ │ │ ├── note.py
│ │ │ │ │ │ ├── project_schema.py
│ │ │ │ │ │ └── user.py
│ │ │ │ │ ├── event/
│ │ │ │ │ │ ├── __init__.py
│ │ │ │ │ │ ├── base.py
│ │ │ │ │ │ ├── expression.py
│ │ │ │ │ │ ├── hub.py
│ │ │ │ │ │ ├── subscriber.py
│ │ │ │ │ │ └── subscription.py
│ │ │ │ │ ├── exception.py
│ │ │ │ │ ├── formatter.py
│ │ │ │ │ ├── inspection.py
│ │ │ │ │ ├── logging.py
│ │ │ │ │ ├── operation.py
│ │ │ │ │ ├── plugin.py
│ │ │ │ │ ├── query.py
│ │ │ │ │ ├── resource_identifier_transformer/
│ │ │ │ │ │ ├── __init__.py
│ │ │ │ │ │ └── base.py
│ │ │ │ │ ├── session.py
│ │ │ │ │ ├── structure/
│ │ │ │ │ │ ├── __init__.py
│ │ │ │ │ │ ├── base.py
│ │ │ │ │ │ ├── entity_id.py
│ │ │ │ │ │ ├── id.py
│ │ │ │ │ │ ├── origin.py
│ │ │ │ │ │ └── standard.py
│ │ │ │ │ └── symbol.py
│ │ │ │ └── test/
│ │ │ │ ├── fixture/
│ │ │ │ │ └── plugin/
│ │ │ │ │ ├── configure_locations.py
│ │ │ │ │ ├── construct_entity_type.py
│ │ │ │ │ └── count_session_event.py
│ │ │ │ └── unit/
│ │ │ │ ├── __init__.py
│ │ │ │ ├── accessor/
│ │ │ │ │ ├── __init__.py
│ │ │ │ │ ├── test_disk.py
│ │ │ │ │ └── test_server.py
│ │ │ │ ├── conftest.py
│ │ │ │ ├── entity/
│ │ │ │ │ ├── __init__.py
│ │ │ │ │ ├── test_asset_version.py
│ │ │ │ │ ├── test_base.py
│ │ │ │ │ ├── test_component.py
│ │ │ │ │ ├── test_factory.py
│ │ │ │ │ ├── test_job.py
│ │ │ │ │ ├── test_location.py
│ │ │ │ │ ├── test_metadata.py
│ │ │ │ │ ├── test_note.py
│ │ │ │ │ ├── test_project_schema.py
│ │ │ │ │ ├── test_scopes.py
│ │ │ │ │ └── test_user.py
│ │ │ │ ├── event/
│ │ │ │ │ ├── __init__.py
│ │ │ │ │ ├── event_hub_server_heartbeat.py
│ │ │ │ │ ├── test_base.py
│ │ │ │ │ ├── test_expression.py
│ │ │ │ │ ├── test_hub.py
│ │ │ │ │ ├── test_subscriber.py
│ │ │ │ │ └── test_subscription.py
│ │ │ │ ├── resource_identifier_transformer/
│ │ │ │ │ ├── __init__.py
│ │ │ │ │ └── test_base.py
│ │ │ │ ├── structure/
│ │ │ │ │ ├── __init__.py
│ │ │ │ │ ├── test_base.py
│ │ │ │ │ ├── test_entity_id.py
│ │ │ │ │ ├── test_id.py
│ │ │ │ │ ├── test_origin.py
│ │ │ │ │ └── test_standard.py
│ │ │ │ ├── test_attribute.py
│ │ │ │ ├── test_cache.py
│ │ │ │ ├── test_collection.py
│ │ │ │ ├── test_custom_attribute.py
│ │ │ │ ├── test_data.py
│ │ │ │ ├── test_formatter.py
│ │ │ │ ├── test_inspection.py
│ │ │ │ ├── test_operation.py
│ │ │ │ ├── test_package.py
│ │ │ │ ├── test_plugin.py
│ │ │ │ ├── test_query.py
│ │ │ │ ├── test_session.py
│ │ │ │ └── test_timer.py
│ │ │ ├── scripts/
│ │ │ │ ├── sub_event_processor.py
│ │ │ │ ├── sub_event_status.py
│ │ │ │ ├── sub_event_storer.py
│ │ │ │ ├── sub_legacy_server.py
│ │ │ │ └── sub_user_server.py
│ │ │ └── tray/
│ │ │ ├── __init__.py
│ │ │ ├── ftrack_tray.py
│ │ │ ├── login_dialog.py
│ │ │ └── login_tools.py
│ │ ├── interfaces.py
│ │ ├── job_queue/
│ │ │ ├── __init__.py
│ │ │ ├── job_server/
│ │ │ │ ├── __init__.py
│ │ │ │ ├── job_queue_route.py
│ │ │ │ ├── jobs.py
│ │ │ │ ├── server.py
│ │ │ │ ├── utils.py
│ │ │ │ ├── workers.py
│ │ │ │ └── workers_rpc_route.py
│ │ │ ├── job_workers/
│ │ │ │ ├── __init__.py
│ │ │ │ └── base_worker.py
│ │ │ └── module.py
│ │ ├── kitsu/
│ │ │ ├── __init__.py
│ │ │ ├── actions/
│ │ │ │ └── launcher_show_in_kitsu.py
│ │ │ ├── kitsu_module.py
│ │ │ ├── kitsu_widgets.py
│ │ │ ├── plugins/
│ │ │ │ └── publish/
│ │ │ │ ├── collect_kitsu_credential.py
│ │ │ │ ├── collect_kitsu_entities.py
│ │ │ │ ├── collect_kitsu_username.py
│ │ │ │ ├── integrate_kitsu_note.py
│ │ │ │ ├── integrate_kitsu_review.py
│ │ │ │ └── other_kitsu_log_out.py
│ │ │ └── utils/
│ │ │ ├── __init__.py
│ │ │ ├── credentials.py
│ │ │ ├── sync_service.py
│ │ │ ├── update_op_with_zou.py
│ │ │ └── update_zou_with_op.py
│ │ ├── launcher_action.py
│ │ ├── log_viewer/
│ │ │ ├── __init__.py
│ │ │ ├── log_view_module.py
│ │ │ └── tray/
│ │ │ ├── __init__.py
│ │ │ ├── app.py
│ │ │ ├── models.py
│ │ │ └── widgets.py
│ │ ├── project_manager_action.py
│ │ ├── python_console_interpreter/
│ │ │ ├── __init__.py
│ │ │ ├── module.py
│ │ │ └── window/
│ │ │ ├── __init__.py
│ │ │ └── widgets.py
│ │ ├── royalrender/
│ │ │ ├── __init__.py
│ │ │ ├── api.py
│ │ │ ├── lib.py
│ │ │ ├── plugins/
│ │ │ │ └── publish/
│ │ │ │ ├── collect_rr_path_from_instance.py
│ │ │ │ ├── collect_sequences_from_job.py
│ │ │ │ ├── create_maya_royalrender_job.py
│ │ │ │ ├── create_nuke_royalrender_job.py
│ │ │ │ ├── create_publish_royalrender_job.py
│ │ │ │ └── submit_jobs_to_royalrender.py
│ │ │ ├── royal_render_module.py
│ │ │ ├── rr_job.py
│ │ │ └── rr_root/
│ │ │ ├── README.md
│ │ │ └── render_apps/
│ │ │ ├── _config/
│ │ │ │ ├── E05__Ayon__PublishJob.cfg
│ │ │ │ └── E05__Ayon___global.inc
│ │ │ ├── _install_paths/
│ │ │ │ └── Ayon.cfg
│ │ │ └── _prepost_scripts/
│ │ │ ├── Ayon_ayon_inject_envvar.cfg
│ │ │ └── ayon_inject_envvar.py
│ │ ├── settings_action.py
│ │ ├── shotgrid/
│ │ │ ├── README.md
│ │ │ ├── __init__.py
│ │ │ ├── lib/
│ │ │ │ ├── __init__.py
│ │ │ │ ├── const.py
│ │ │ │ ├── credentials.py
│ │ │ │ ├── record.py
│ │ │ │ └── settings.py
│ │ │ ├── plugins/
│ │ │ │ └── publish/
│ │ │ │ ├── collect_shotgrid_entities.py
│ │ │ │ ├── collect_shotgrid_session.py
│ │ │ │ ├── integrate_shotgrid_publish.py
│ │ │ │ ├── integrate_shotgrid_version.py
│ │ │ │ └── validate_shotgrid_user.py
│ │ │ ├── server/
│ │ │ │ └── README.md
│ │ │ ├── shotgrid_module.py
│ │ │ ├── tests/
│ │ │ │ └── shotgrid/
│ │ │ │ └── lib/
│ │ │ │ └── test_credentials.py
│ │ │ └── tray/
│ │ │ ├── credential_dialog.py
│ │ │ └── shotgrid_tray.py
│ │ ├── slack/
│ │ │ ├── README.md
│ │ │ ├── __init__.py
│ │ │ ├── launch_hooks/
│ │ │ │ └── pre_python2_vendor.py
│ │ │ ├── manifest.yml
│ │ │ ├── plugins/
│ │ │ │ └── publish/
│ │ │ │ ├── collect_slack_family.py
│ │ │ │ └── integrate_slack_api.py
│ │ │ ├── python2_vendor/
│ │ │ │ └── python-slack-sdk-1/
│ │ │ │ ├── .appveyor.yml
│ │ │ │ ├── .coveragerc
│ │ │ │ ├── .flake8
│ │ │ │ ├── .github/
│ │ │ │ │ ├── contributing.md
│ │ │ │ │ ├── issue_template.md
│ │ │ │ │ ├── maintainers_guide.md
│ │ │ │ │ └── pull_request_template.md
│ │ │ │ ├── .gitignore
│ │ │ │ ├── .travis.yml
│ │ │ │ ├── LICENSE
│ │ │ │ ├── MANIFEST.in
│ │ │ │ ├── README.rst
│ │ │ │ ├── docs/
│ │ │ │ │ ├── .buildinfo
│ │ │ │ │ ├── .nojekyll
│ │ │ │ │ ├── _static/
│ │ │ │ │ │ ├── basic.css
│ │ │ │ │ │ ├── classic.css
│ │ │ │ │ │ ├── default.css
│ │ │ │ │ │ ├── docs.css
│ │ │ │ │ │ ├── doctools.js
│ │ │ │ │ │ ├── documentation_options.js
│ │ │ │ │ │ ├── jquery-3.2.1.js
│ │ │ │ │ │ ├── jquery.js
│ │ │ │ │ │ ├── language_data.js
│ │ │ │ │ │ ├── pygments.css
│ │ │ │ │ │ ├── searchtools.js
│ │ │ │ │ │ ├── sidebar.js
│ │ │ │ │ │ ├── underscore-1.3.1.js
│ │ │ │ │ │ ├── underscore.js
│ │ │ │ │ │ └── websupport.js
│ │ │ │ │ ├── about.html
│ │ │ │ │ ├── auth.html
│ │ │ │ │ ├── basic_usage.html
│ │ │ │ │ ├── changelog.html
│ │ │ │ │ ├── conversations.html
│ │ │ │ │ ├── faq.html
│ │ │ │ │ ├── genindex.html
│ │ │ │ │ ├── index.html
│ │ │ │ │ ├── metadata.html
│ │ │ │ │ ├── objects.inv
│ │ │ │ │ ├── real_time_messaging.html
│ │ │ │ │ ├── search.html
│ │ │ │ │ └── searchindex.js
│ │ │ │ ├── docs-src/
│ │ │ │ │ ├── .gitignore
│ │ │ │ │ ├── Makefile
│ │ │ │ │ ├── _themes/
│ │ │ │ │ │ └── slack/
│ │ │ │ │ │ ├── conf.py
│ │ │ │ │ │ ├── layout.html
│ │ │ │ │ │ ├── localtoc.html
│ │ │ │ │ │ ├── relations.html
│ │ │ │ │ │ ├── sidebar.html
│ │ │ │ │ │ ├── static/
│ │ │ │ │ │ │ ├── default.css_t
│ │ │ │ │ │ │ ├── docs.css_t
│ │ │ │ │ │ │ └── pygments.css_t
│ │ │ │ │ │ └── theme.conf
│ │ │ │ │ ├── about.rst
│ │ │ │ │ ├── auth.rst
│ │ │ │ │ ├── basic_usage.rst
│ │ │ │ │ ├── changelog.rst
│ │ │ │ │ ├── conf.py
│ │ │ │ │ ├── conversations.rst
│ │ │ │ │ ├── faq.rst
│ │ │ │ │ ├── index.rst
│ │ │ │ │ ├── make.bat
│ │ │ │ │ ├── metadata.rst
│ │ │ │ │ └── real_time_messaging.rst
│ │ │ │ ├── docs.sh
│ │ │ │ ├── requirements.txt
│ │ │ │ ├── setup.cfg
│ │ │ │ ├── setup.py
│ │ │ │ ├── slackclient/
│ │ │ │ │ ├── __init__.py
│ │ │ │ │ ├── channel.py
│ │ │ │ │ ├── client.py
│ │ │ │ │ ├── exceptions.py
│ │ │ │ │ ├── im.py
│ │ │ │ │ ├── server.py
│ │ │ │ │ ├── slackrequest.py
│ │ │ │ │ ├── user.py
│ │ │ │ │ ├── util.py
│ │ │ │ │ └── version.py
│ │ │ │ ├── test_requirements.txt
│ │ │ │ ├── tests/
│ │ │ │ │ ├── conftest.py
│ │ │ │ │ ├── data/
│ │ │ │ │ │ ├── channel.created.json
│ │ │ │ │ │ ├── im.created.json
│ │ │ │ │ │ └── rtm.start.json
│ │ │ │ │ ├── test_channel.py
│ │ │ │ │ ├── test_server.py
│ │ │ │ │ ├── test_slackclient.py
│ │ │ │ │ └── test_slackrequest.py
│ │ │ │ └── tox.ini
│ │ │ └── slack_module.py
│ │ ├── sync_server/
│ │ │ ├── README.md
│ │ │ ├── __init__.py
│ │ │ ├── launch_hooks/
│ │ │ │ └── pre_copy_last_published_workfile.py
│ │ │ ├── plugins/
│ │ │ │ └── load/
│ │ │ │ ├── add_site.py
│ │ │ │ └── remove_site.py
│ │ │ ├── providers/
│ │ │ │ ├── __init__.py
│ │ │ │ ├── abstract_provider.py
│ │ │ │ ├── dropbox.py
│ │ │ │ ├── gdrive.py
│ │ │ │ ├── lib.py
│ │ │ │ ├── local_drive.py
│ │ │ │ └── sftp.py
│ │ │ ├── rest_api.py
│ │ │ ├── sync_server.py
│ │ │ ├── sync_server_module.py
│ │ │ ├── tray/
│ │ │ │ ├── app.py
│ │ │ │ ├── delegates.py
│ │ │ │ ├── lib.py
│ │ │ │ ├── models.py
│ │ │ │ └── widgets.py
│ │ │ └── utils.py
│ │ ├── timers_manager/
│ │ │ ├── __init__.py
│ │ │ ├── exceptions.py
│ │ │ ├── idle_threads.py
│ │ │ ├── launch_hooks/
│ │ │ │ └── post_start_timer.py
│ │ │ ├── plugins/
│ │ │ │ └── publish/
│ │ │ │ ├── start_timer.py
│ │ │ │ └── stop_timer.py
│ │ │ ├── rest_api.py
│ │ │ ├── timers_manager.py
│ │ │ └── widget_user_idle.py
│ │ └── webserver/
│ │ ├── __init__.py
│ │ ├── base_routes.py
│ │ ├── cors_middleware.py
│ │ ├── host_console_listener.py
│ │ ├── server.py
│ │ └── webserver_module.py
│ ├── pipeline/
│ │ ├── __init__.py
│ │ ├── actions.py
│ │ ├── anatomy.py
│ │ ├── colorspace.py
│ │ ├── constants.py
│ │ ├── context_tools.py
│ │ ├── create/
│ │ │ ├── README.md
│ │ │ ├── __init__.py
│ │ │ ├── constants.py
│ │ │ ├── context.py
│ │ │ ├── creator_plugins.py
│ │ │ ├── legacy_create.py
│ │ │ ├── subset_name.py
│ │ │ └── utils.py
│ │ ├── delivery.py
│ │ ├── editorial.py
│ │ ├── farm/
│ │ │ ├── __init__.py
│ │ │ ├── patterning.py
│ │ │ ├── pyblish_functions.py
│ │ │ ├── pyblish_functions.pyi
│ │ │ └── tools.py
│ │ ├── legacy_io.py
│ │ ├── load/
│ │ │ ├── __init__.py
│ │ │ ├── plugins.py
│ │ │ └── utils.py
│ │ ├── mongodb.py
│ │ ├── plugin_discover.py
│ │ ├── project_folders.py
│ │ ├── publish/
│ │ │ ├── README.md
│ │ │ ├── __init__.py
│ │ │ ├── abstract_collect_render.py
│ │ │ ├── abstract_expected_files.py
│ │ │ ├── constants.py
│ │ │ ├── lib.py
│ │ │ └── publish_plugins.py
│ │ ├── schema/
│ │ │ ├── __init__.py
│ │ │ ├── application-1.0.json
│ │ │ ├── asset-1.0.json
│ │ │ ├── asset-2.0.json
│ │ │ ├── asset-3.0.json
│ │ │ ├── config-1.0.json
│ │ │ ├── config-1.1.json
│ │ │ ├── config-2.0.json
│ │ │ ├── container-1.0.json
│ │ │ ├── container-2.0.json
│ │ │ ├── hero_version-1.0.json
│ │ │ ├── inventory-1.0.json
│ │ │ ├── inventory-1.1.json
│ │ │ ├── project-2.0.json
│ │ │ ├── project-2.1.json
│ │ │ ├── project-3.0.json
│ │ │ ├── representation-1.0.json
│ │ │ ├── representation-2.0.json
│ │ │ ├── session-1.0.json
│ │ │ ├── session-2.0.json
│ │ │ ├── session-3.0.json
│ │ │ ├── session-4.0.json
│ │ │ ├── shaders-1.0.json
│ │ │ ├── subset-1.0.json
│ │ │ ├── subset-2.0.json
│ │ │ ├── subset-3.0.json
│ │ │ ├── thumbnail-1.0.json
│ │ │ ├── version-1.0.json
│ │ │ ├── version-2.0.json
│ │ │ ├── version-3.0.json
│ │ │ └── workfile-1.0.json
│ │ ├── tempdir.py
│ │ ├── template_data.py
│ │ ├── thumbnail.py
│ │ ├── version_start.py
│ │ └── workfile/
│ │ ├── __init__.py
│ │ ├── build_workfile.py
│ │ ├── lock_workfile.py
│ │ ├── path_resolving.py
│ │ └── workfile_template_builder.py
│ ├── plugins/
│ │ ├── actions/
│ │ │ └── open_file_explorer.py
│ │ ├── inventory/
│ │ │ └── remove_and_load.py
│ │ ├── load/
│ │ │ ├── copy_file.py
│ │ │ ├── copy_file_path.py
│ │ │ ├── delete_old_versions.py
│ │ │ ├── delivery.py
│ │ │ ├── open_djv.py
│ │ │ ├── open_file.py
│ │ │ └── push_to_library.py
│ │ └── publish/
│ │ ├── cleanup.py
│ │ ├── cleanup_explicit.py
│ │ ├── cleanup_farm.py
│ │ ├── collect_anatomy_context_data.py
│ │ ├── collect_anatomy_instance_data.py
│ │ ├── collect_anatomy_object.py
│ │ ├── collect_audio.py
│ │ ├── collect_cleanup_keys.py
│ │ ├── collect_comment.py
│ │ ├── collect_context_entities.py
│ │ ├── collect_context_label.py
│ │ ├── collect_current_context.py
│ │ ├── collect_current_pype_user.py
│ │ ├── collect_current_shell_file.py
│ │ ├── collect_custom_staging_dir.py
│ │ ├── collect_datetime_data.py
│ │ ├── collect_farm_target.py
│ │ ├── collect_frames_fix.py
│ │ ├── collect_from_create_context.py
│ │ ├── collect_hierarchy.py
│ │ ├── collect_host_name.py
│ │ ├── collect_input_representations_to_versions.py
│ │ ├── collect_machine_name.py
│ │ ├── collect_modules.py
│ │ ├── collect_otio_frame_ranges.py
│ │ ├── collect_otio_review.py
│ │ ├── collect_otio_subset_resources.py
│ │ ├── collect_rendered_files.py
│ │ ├── collect_resources_path.py
│ │ ├── collect_scene_loaded_versions.py
│ │ ├── collect_scene_version.py
│ │ ├── collect_settings.py
│ │ ├── collect_shell_workspace.py
│ │ ├── collect_source_for_source.py
│ │ ├── collect_time.py
│ │ ├── extract_burnin.py
│ │ ├── extract_color_transcode.py
│ │ ├── extract_colorspace_data.py
│ │ ├── extract_hierarchy_avalon.py
│ │ ├── extract_hierarchy_to_ayon.py
│ │ ├── extract_otio_audio_tracks.py
│ │ ├── extract_otio_file.py
│ │ ├── extract_otio_review.py
│ │ ├── extract_otio_trimming_video.py
│ │ ├── extract_review.py
│ │ ├── extract_review_slate.py
│ │ ├── extract_scanline_exr.py
│ │ ├── extract_thumbnail.py
│ │ ├── extract_thumbnail_from_source.py
│ │ ├── extract_trim_video_audio.py
│ │ ├── help/
│ │ │ ├── validate_containers.xml
│ │ │ ├── validate_publish_dir.xml
│ │ │ └── validate_unique_subsets.xml
│ │ ├── integrate.py
│ │ ├── integrate_hero_version.py
│ │ ├── integrate_inputlinks.py
│ │ ├── integrate_inputlinks_ayon.py
│ │ ├── integrate_resources_path.py
│ │ ├── integrate_subset_group.py
│ │ ├── integrate_thumbnail.py
│ │ ├── integrate_thumbnail_ayon.py
│ │ ├── integrate_version_attrs.py
│ │ ├── preintegrate_thumbnail_representation.py
│ │ ├── repair_unicode_strings.py
│ │ ├── validate_asset_docs.py
│ │ ├── validate_containers.py
│ │ ├── validate_editorial_asset_name.py
│ │ ├── validate_file_saved.py
│ │ ├── validate_filesequences.py
│ │ ├── validate_intent.py
│ │ ├── validate_publish_dir.py
│ │ ├── validate_resources.py
│ │ ├── validate_unique_subsets.py
│ │ └── validate_version.py
│ ├── pype_commands.py
│ ├── resources/
│ │ ├── __init__.py
│ │ ├── fonts/
│ │ │ └── LiberationSans/
│ │ │ └── License.txt
│ │ └── ftrack/
│ │ └── sign_in_message.html
│ ├── scripts/
│ │ ├── __init__.py
│ │ ├── non_python_host_launch.py
│ │ ├── ocio_wrapper.py
│ │ ├── otio_burnin.py
│ │ ├── remote_publish.py
│ │ └── slates/
│ │ ├── __init__.py
│ │ ├── __main__.py
│ │ └── slate_base/
│ │ ├── __init__.py
│ │ ├── api.py
│ │ ├── base.py
│ │ ├── default_style.json
│ │ ├── example.py
│ │ ├── font_factory.py
│ │ ├── items.py
│ │ ├── layer.py
│ │ ├── lib.py
│ │ └── main_frame.py
│ ├── settings/
│ │ ├── __init__.py
│ │ ├── ayon_settings.py
│ │ ├── constants.py
│ │ ├── defaults/
│ │ │ ├── project_anatomy/
│ │ │ │ ├── attributes.json
│ │ │ │ ├── imageio.json
│ │ │ │ ├── roots.json
│ │ │ │ ├── tasks.json
│ │ │ │ └── templates.json
│ │ │ ├── project_settings/
│ │ │ │ ├── aftereffects.json
│ │ │ │ ├── applications.json
│ │ │ │ ├── blender.json
│ │ │ │ ├── celaction.json
│ │ │ │ ├── deadline.json
│ │ │ │ ├── equalizer.json
│ │ │ │ ├── flame.json
│ │ │ │ ├── ftrack.json
│ │ │ │ ├── fusion.json
│ │ │ │ ├── global.json
│ │ │ │ ├── harmony.json
│ │ │ │ ├── hiero.json
│ │ │ │ ├── houdini.json
│ │ │ │ ├── kitsu.json
│ │ │ │ ├── max.json
│ │ │ │ ├── maya.json
│ │ │ │ ├── nuke.json
│ │ │ │ ├── photoshop.json
│ │ │ │ ├── resolve.json
│ │ │ │ ├── royalrender.json
│ │ │ │ ├── shotgrid.json
│ │ │ │ ├── slack.json
│ │ │ │ ├── standalonepublisher.json
│ │ │ │ ├── substancepainter.json
│ │ │ │ ├── traypublisher.json
│ │ │ │ ├── tvpaint.json
│ │ │ │ ├── unreal.json
│ │ │ │ └── webpublisher.json
│ │ │ └── system_settings/
│ │ │ ├── applications.json
│ │ │ ├── general.json
│ │ │ ├── modules.json
│ │ │ └── tools.json
│ │ ├── entities/
│ │ │ ├── __init__.py
│ │ │ ├── anatomy_entities.py
│ │ │ ├── base_entity.py
│ │ │ ├── color_entity.py
│ │ │ ├── dict_conditional.py
│ │ │ ├── dict_immutable_keys_entity.py
│ │ │ ├── dict_mutable_keys_entity.py
│ │ │ ├── enum_entity.py
│ │ │ ├── exceptions.py
│ │ │ ├── input_entities.py
│ │ │ ├── item_entities.py
│ │ │ ├── lib.py
│ │ │ ├── list_entity.py
│ │ │ ├── op_version_entity.py
│ │ │ ├── root_entities.py
│ │ │ └── schemas/
│ │ │ ├── README.md
│ │ │ ├── projects_schema/
│ │ │ │ ├── schema_main.json
│ │ │ │ ├── schema_project_aftereffects.json
│ │ │ │ ├── schema_project_applications.json
│ │ │ │ ├── schema_project_blender.json
│ │ │ │ ├── schema_project_celaction.json
│ │ │ │ ├── schema_project_deadline.json
│ │ │ │ ├── schema_project_equalizer.json
│ │ │ │ ├── schema_project_flame.json
│ │ │ │ ├── schema_project_ftrack.json
│ │ │ │ ├── schema_project_fusion.json
│ │ │ │ ├── schema_project_global.json
│ │ │ │ ├── schema_project_harmony.json
│ │ │ │ ├── schema_project_hiero.json
│ │ │ │ ├── schema_project_houdini.json
│ │ │ │ ├── schema_project_kitsu.json
│ │ │ │ ├── schema_project_max.json
│ │ │ │ ├── schema_project_maya.json
│ │ │ │ ├── schema_project_nuke.json
│ │ │ │ ├── schema_project_photoshop.json
│ │ │ │ ├── schema_project_resolve.json
│ │ │ │ ├── schema_project_royalrender.json
│ │ │ │ ├── schema_project_shotgrid.json
│ │ │ │ ├── schema_project_slack.json
│ │ │ │ ├── schema_project_standalonepublisher.json
│ │ │ │ ├── schema_project_substancepainter.json
│ │ │ │ ├── schema_project_syncserver.json
│ │ │ │ ├── schema_project_traypublisher.json
│ │ │ │ ├── schema_project_tvpaint.json
│ │ │ │ ├── schema_project_unreal.json
│ │ │ │ ├── schema_project_webpublisher.json
│ │ │ │ └── schemas/
│ │ │ │ ├── schema_anatomy_attributes.json
│ │ │ │ ├── schema_anatomy_imageio.json
│ │ │ │ ├── schema_anatomy_templates.json
│ │ │ │ ├── schema_blender_publish.json
│ │ │ │ ├── schema_equalizer_create.json
│ │ │ │ ├── schema_global_publish.json
│ │ │ │ ├── schema_global_tools.json
│ │ │ │ ├── schema_houdini_create.json
│ │ │ │ ├── schema_houdini_general.json
│ │ │ │ ├── schema_houdini_publish.json
│ │ │ │ ├── schema_houdini_scriptshelf.json
│ │ │ │ ├── schema_max_publish.json
│ │ │ │ ├── schema_maya_capture.json
│ │ │ │ ├── schema_maya_create.json
│ │ │ │ ├── schema_maya_load.json
│ │ │ │ ├── schema_maya_publish.json
│ │ │ │ ├── schema_maya_render_settings.json
│ │ │ │ ├── schema_nuke_imageio.json
│ │ │ │ ├── schema_nuke_load.json
│ │ │ │ ├── schema_nuke_publish.json
│ │ │ │ ├── schema_nuke_scriptsgizmo.json
│ │ │ │ ├── schema_publish_gui_filter.json
│ │ │ │ ├── schema_representation_tags.json
│ │ │ │ ├── schema_scriptsmenu.json
│ │ │ │ ├── schema_templated_workfile_build.json
│ │ │ │ ├── schema_workfile_build.json
│ │ │ │ ├── template_colorspace_remapping.json
│ │ │ │ ├── template_create_plugin.json
│ │ │ │ ├── template_host_color_management_derived.json
│ │ │ │ ├── template_host_color_management_ocio.json
│ │ │ │ ├── template_host_color_management_remapped.json
│ │ │ │ ├── template_imageio_config.json
│ │ │ │ ├── template_imageio_file_rules.json
│ │ │ │ ├── template_loader_plugin_nuke.json
│ │ │ │ ├── template_nuke_knob_inputs.json
│ │ │ │ ├── template_nuke_write_attrs.json
│ │ │ │ ├── template_publish_families.json
│ │ │ │ ├── template_publish_plugin.json
│ │ │ │ ├── template_validate_plugin.json
│ │ │ │ ├── template_workfile_builder_simple.json
│ │ │ │ └── template_workfile_options.json
│ │ │ └── system_schema/
│ │ │ ├── example_infinite_hierarchy.json
│ │ │ ├── example_schema.json
│ │ │ ├── example_template.json
│ │ │ ├── host_settings/
│ │ │ │ ├── schema_3dequalizer.json
│ │ │ │ ├── schema_3dsmax.json
│ │ │ │ ├── schema_aftereffects.json
│ │ │ │ ├── schema_blender.json
│ │ │ │ ├── schema_celaction.json
│ │ │ │ ├── schema_djv.json
│ │ │ │ ├── schema_flame.json
│ │ │ │ ├── schema_fusion.json
│ │ │ │ ├── schema_harmony.json
│ │ │ │ ├── schema_houdini.json
│ │ │ │ ├── schema_maya.json
│ │ │ │ ├── schema_mayapy.json
│ │ │ │ ├── schema_photoshop.json
│ │ │ │ ├── schema_resolve.json
│ │ │ │ ├── schema_substancepainter.json
│ │ │ │ ├── schema_tvpaint.json
│ │ │ │ ├── schema_unreal.json
│ │ │ │ ├── template_host_unchangables.json
│ │ │ │ ├── template_host_variant.json
│ │ │ │ ├── template_host_variant_items.json
│ │ │ │ └── template_nuke.json
│ │ │ ├── module_settings/
│ │ │ │ ├── schema_ftrack.json
│ │ │ │ ├── schema_kitsu.json
│ │ │ │ └── template_custom_attribute.json
│ │ │ ├── schema_applications.json
│ │ │ ├── schema_general.json
│ │ │ ├── schema_main.json
│ │ │ ├── schema_modules.json
│ │ │ └── schema_tools.json
│ │ ├── exceptions.py
│ │ ├── handlers.py
│ │ ├── lib.py
│ │ └── local_settings.md
│ ├── style/
│ │ ├── __init__.py
│ │ ├── color_defs.py
│ │ ├── data.json
│ │ ├── fonts/
│ │ │ ├── Noto_Sans/
│ │ │ │ └── OFL.txt
│ │ │ └── Noto_Sans_Mono/
│ │ │ ├── OFL.txt
│ │ │ └── README.txt
│ │ ├── pyqt5_resources.py
│ │ ├── pyside2_resources.py
│ │ ├── pyside6_resources.py
│ │ ├── qrc_resources.py
│ │ ├── resources.qrc
│ │ └── style.css
│ ├── tests/
│ │ ├── README.md
│ │ ├── __init__.py
│ │ ├── lib.py
│ │ ├── mongo_performance.py
│ │ ├── test_avalon_plugin_presets.py
│ │ ├── test_lib_restructuralization.py
│ │ └── test_pyblish_filter.py
│ ├── tools/
│ │ ├── __init__.py
│ │ ├── adobe_webserver/
│ │ │ ├── app.py
│ │ │ └── readme.txt
│ │ ├── assetlinks/
│ │ │ ├── __init__.py
│ │ │ └── widgets.py
│ │ ├── attribute_defs/
│ │ │ ├── __init__.py
│ │ │ ├── dialog.py
│ │ │ ├── files_widget.py
│ │ │ └── widgets.py
│ │ ├── ayon_launcher/
│ │ │ ├── abstract.py
│ │ │ ├── control.py
│ │ │ ├── models/
│ │ │ │ ├── __init__.py
│ │ │ │ ├── actions.py
│ │ │ │ └── selection.py
│ │ │ └── ui/
│ │ │ ├── __init__.py
│ │ │ ├── actions_widget.py
│ │ │ ├── hierarchy_page.py
│ │ │ ├── projects_widget.py
│ │ │ ├── resources/
│ │ │ │ └── __init__.py
│ │ │ └── window.py
│ │ ├── ayon_loader/
│ │ │ ├── __init__.py
│ │ │ ├── abstract.py
│ │ │ ├── control.py
│ │ │ ├── models/
│ │ │ │ ├── __init__.py
│ │ │ │ ├── actions.py
│ │ │ │ ├── products.py
│ │ │ │ ├── selection.py
│ │ │ │ └── site_sync.py
│ │ │ └── ui/
│ │ │ ├── __init__.py
│ │ │ ├── actions_utils.py
│ │ │ ├── folders_widget.py
│ │ │ ├── info_widget.py
│ │ │ ├── product_group_dialog.py
│ │ │ ├── product_types_widget.py
│ │ │ ├── products_delegates.py
│ │ │ ├── products_model.py
│ │ │ ├── products_widget.py
│ │ │ ├── repres_widget.py
│ │ │ └── window.py
│ │ ├── ayon_push_to_project/
│ │ │ ├── __init__.py
│ │ │ ├── control.py
│ │ │ ├── main.py
│ │ │ ├── models/
│ │ │ │ ├── __init__.py
│ │ │ │ ├── integrate.py
│ │ │ │ ├── selection.py
│ │ │ │ └── user_values.py
│ │ │ └── ui/
│ │ │ ├── __init__.py
│ │ │ └── window.py
│ │ ├── ayon_sceneinventory/
│ │ │ ├── __init__.py
│ │ │ ├── control.py
│ │ │ ├── model.py
│ │ │ ├── models/
│ │ │ │ ├── __init__.py
│ │ │ │ └── site_sync.py
│ │ │ ├── switch_dialog/
│ │ │ │ ├── __init__.py
│ │ │ │ ├── dialog.py
│ │ │ │ ├── folders_input.py
│ │ │ │ └── widgets.py
│ │ │ ├── view.py
│ │ │ └── window.py
│ │ ├── ayon_utils/
│ │ │ ├── models/
│ │ │ │ ├── __init__.py
│ │ │ │ ├── cache.py
│ │ │ │ ├── hierarchy.py
│ │ │ │ ├── projects.py
│ │ │ │ ├── selection.py
│ │ │ │ └── thumbnails.py
│ │ │ └── widgets/
│ │ │ ├── __init__.py
│ │ │ ├── folders_widget.py
│ │ │ ├── projects_widget.py
│ │ │ ├── tasks_widget.py
│ │ │ └── utils.py
│ │ ├── ayon_workfiles/
│ │ │ ├── __init__.py
│ │ │ ├── abstract.py
│ │ │ ├── control.py
│ │ │ ├── models/
│ │ │ │ ├── __init__.py
│ │ │ │ ├── selection.py
│ │ │ │ └── workfiles.py
│ │ │ └── widgets/
│ │ │ ├── __init__.py
│ │ │ ├── constants.py
│ │ │ ├── files_widget.py
│ │ │ ├── files_widget_published.py
│ │ │ ├── files_widget_workarea.py
│ │ │ ├── save_as_dialog.py
│ │ │ ├── side_panel.py
│ │ │ ├── utils.py
│ │ │ └── window.py
│ │ ├── context_dialog/
│ │ │ ├── __init__.py
│ │ │ ├── _ayon_window.py
│ │ │ └── _openpype_window.py
│ │ ├── creator/
│ │ │ ├── __init__.py
│ │ │ ├── constants.py
│ │ │ ├── model.py
│ │ │ ├── widgets.py
│ │ │ └── window.py
│ │ ├── experimental_tools/
│ │ │ ├── __init__.py
│ │ │ ├── dialog.py
│ │ │ └── tools_def.py
│ │ ├── flickcharm.py
│ │ ├── launcher/
│ │ │ ├── __init__.py
│ │ │ ├── actions.py
│ │ │ ├── constants.py
│ │ │ ├── delegates.py
│ │ │ ├── lib.py
│ │ │ ├── models.py
│ │ │ ├── widgets.py
│ │ │ └── window.py
│ │ ├── libraryloader/
│ │ │ ├── __init__.py
│ │ │ ├── __main__.py
│ │ │ └── app.py
│ │ ├── loader/
│ │ │ ├── __init__.py
│ │ │ ├── __main__.py
│ │ │ ├── app.py
│ │ │ ├── delegates.py
│ │ │ ├── lib.py
│ │ │ ├── model.py
│ │ │ └── widgets.py
│ │ ├── project_manager/
│ │ │ ├── __init__.py
│ │ │ ├── __main__.py
│ │ │ └── project_manager/
│ │ │ ├── __init__.py
│ │ │ ├── constants.py
│ │ │ ├── delegates.py
│ │ │ ├── model.py
│ │ │ ├── multiselection_combobox.py
│ │ │ ├── style.py
│ │ │ ├── view.py
│ │ │ ├── widgets.py
│ │ │ └── window.py
│ │ ├── publisher/
│ │ │ ├── __init__.py
│ │ │ ├── app.py
│ │ │ ├── constants.py
│ │ │ ├── control.py
│ │ │ ├── control_qt.py
│ │ │ ├── publish_report_viewer/
│ │ │ │ ├── __init__.py
│ │ │ │ ├── constants.py
│ │ │ │ ├── delegates.py
│ │ │ │ ├── model.py
│ │ │ │ ├── report_items.py
│ │ │ │ ├── widgets.py
│ │ │ │ └── window.py
│ │ │ ├── widgets/
│ │ │ │ ├── __init__.py
│ │ │ │ ├── assets_widget.py
│ │ │ │ ├── border_label_widget.py
│ │ │ │ ├── card_view_widgets.py
│ │ │ │ ├── create_widget.py
│ │ │ │ ├── help_widget.py
│ │ │ │ ├── icons.py
│ │ │ │ ├── list_view_widgets.py
│ │ │ │ ├── overview_widget.py
│ │ │ │ ├── precreate_widget.py
│ │ │ │ ├── publish_frame.py
│ │ │ │ ├── report_page.py
│ │ │ │ ├── screenshot_widget.py
│ │ │ │ ├── tabs_widget.py
│ │ │ │ ├── tasks_widget.py
│ │ │ │ ├── thumbnail_widget.py
│ │ │ │ └── widgets.py
│ │ │ └── window.py
│ │ ├── push_to_project/
│ │ │ ├── __init__.py
│ │ │ ├── app.py
│ │ │ ├── control_context.py
│ │ │ ├── control_integrate.py
│ │ │ └── window.py
│ │ ├── pyblish_pype/
│ │ │ ├── __init__.py
│ │ │ ├── __main__.py
│ │ │ ├── app.css
│ │ │ ├── app.py
│ │ │ ├── awesome.py
│ │ │ ├── constants.py
│ │ │ ├── control.py
│ │ │ ├── delegate.py
│ │ │ ├── font/
│ │ │ │ └── opensans/
│ │ │ │ └── LICENSE.txt
│ │ │ ├── i18n/
│ │ │ │ ├── pyblish_lite.pro
│ │ │ │ ├── zh_CN.qm
│ │ │ │ └── zh_CN.ts
│ │ │ ├── mock.py
│ │ │ ├── model.py
│ │ │ ├── settings.py
│ │ │ ├── util.py
│ │ │ ├── vendor/
│ │ │ │ ├── __init__.py
│ │ │ │ └── qtawesome/
│ │ │ │ ├── __init__.py
│ │ │ │ ├── _version.py
│ │ │ │ ├── animation.py
│ │ │ │ ├── fonts/
│ │ │ │ │ ├── elusiveicons-webfont-charmap.json
│ │ │ │ │ └── fontawesome-webfont-charmap.json
│ │ │ │ └── iconic_font.py
│ │ │ ├── version.py
│ │ │ ├── view.py
│ │ │ ├── widgets.py
│ │ │ └── window.py
│ │ ├── repack_version.py
│ │ ├── resources/
│ │ │ └── __init__.py
│ │ ├── sceneinventory/
│ │ │ ├── __init__.py
│ │ │ ├── lib.py
│ │ │ ├── model.py
│ │ │ ├── switch_dialog.py
│ │ │ ├── view.py
│ │ │ ├── widgets.py
│ │ │ └── window.py
│ │ ├── settings/
│ │ │ ├── __init__.py
│ │ │ ├── __main__.py
│ │ │ ├── lib.py
│ │ │ ├── local_settings/
│ │ │ │ ├── __init__.py
│ │ │ │ ├── apps_widget.py
│ │ │ │ ├── constants.py
│ │ │ │ ├── environments_widget.py
│ │ │ │ ├── experimental_widget.py
│ │ │ │ ├── general_widget.py
│ │ │ │ ├── mongo_widget.py
│ │ │ │ ├── projects_widget.py
│ │ │ │ ├── widgets.py
│ │ │ │ └── window.py
│ │ │ ├── resources/
│ │ │ │ └── __init__.py
│ │ │ └── settings/
│ │ │ ├── README.md
│ │ │ ├── __init__.py
│ │ │ ├── base.py
│ │ │ ├── breadcrumbs_widget.py
│ │ │ ├── categories.py
│ │ │ ├── color_widget.py
│ │ │ ├── constants.py
│ │ │ ├── dialogs.py
│ │ │ ├── dict_conditional.py
│ │ │ ├── dict_mutable_widget.py
│ │ │ ├── images/
│ │ │ │ └── __init__.py
│ │ │ ├── item_widgets.py
│ │ │ ├── lib.py
│ │ │ ├── list_item_widget.py
│ │ │ ├── list_strict_widget.py
│ │ │ ├── search_dialog.py
│ │ │ ├── tests.py
│ │ │ ├── widgets.py
│ │ │ ├── window.py
│ │ │ └── wrapper_widgets.py
│ │ ├── standalonepublish/
│ │ │ ├── __init__.py
│ │ │ ├── app.py
│ │ │ ├── publish.py
│ │ │ └── widgets/
│ │ │ ├── __init__.py
│ │ │ ├── constants.py
│ │ │ ├── model_asset.py
│ │ │ ├── model_filter_proxy_exact_match.py
│ │ │ ├── model_filter_proxy_recursive_sort.py
│ │ │ ├── model_node.py
│ │ │ ├── model_tasks_template.py
│ │ │ ├── model_tree.py
│ │ │ ├── model_tree_view_deselectable.py
│ │ │ ├── resources/
│ │ │ │ └── __init__.py
│ │ │ ├── widget_asset.py
│ │ │ ├── widget_component_item.py
│ │ │ ├── widget_components.py
│ │ │ ├── widget_components_list.py
│ │ │ ├── widget_drop_empty.py
│ │ │ ├── widget_drop_frame.py
│ │ │ ├── widget_family.py
│ │ │ ├── widget_family_desc.py
│ │ │ └── widget_shadow.py
│ │ ├── stdout_broker/
│ │ │ ├── __init__.py
│ │ │ ├── app.py
│ │ │ └── window.py
│ │ ├── subsetmanager/
│ │ │ ├── README.md
│ │ │ ├── __init__.py
│ │ │ ├── model.py
│ │ │ ├── widgets.py
│ │ │ └── window.py
│ │ ├── texture_copy/
│ │ │ └── app.py
│ │ ├── tray/
│ │ │ ├── __init__.py
│ │ │ ├── __main__.py
│ │ │ ├── pype_info_widget.py
│ │ │ └── pype_tray.py
│ │ ├── traypublisher/
│ │ │ ├── __init__.py
│ │ │ └── window.py
│ │ ├── utils/
│ │ │ ├── __init__.py
│ │ │ ├── assets_widget.py
│ │ │ ├── constants.py
│ │ │ ├── delegates.py
│ │ │ ├── error_dialog.py
│ │ │ ├── host_tools.py
│ │ │ ├── images/
│ │ │ │ └── __init__.py
│ │ │ ├── layouts.py
│ │ │ ├── lib.py
│ │ │ ├── models.py
│ │ │ ├── multiselection_combobox.py
│ │ │ ├── overlay_messages.py
│ │ │ ├── tasks_widget.py
│ │ │ ├── thumbnail_paint_widget.py
│ │ │ ├── views.py
│ │ │ └── widgets.py
│ │ ├── workfile_template_build/
│ │ │ ├── __init__.py
│ │ │ └── window.py
│ │ └── workfiles/
│ │ ├── README.md
│ │ ├── __init__.py
│ │ ├── app.py
│ │ ├── files_widget.py
│ │ ├── lock_dialog.py
│ │ ├── model.py
│ │ ├── save_as_dialog.py
│ │ └── window.py
│ ├── vendor/
│ │ ├── __init__.py
│ │ └── python/
│ │ ├── common/
│ │ │ ├── README.md
│ │ │ ├── ayon_api/
│ │ │ │ ├── __init__.py
│ │ │ │ ├── _api.py
│ │ │ │ ├── constants.py
│ │ │ │ ├── entity_hub.py
│ │ │ │ ├── events.py
│ │ │ │ ├── exceptions.py
│ │ │ │ ├── graphql.py
│ │ │ │ ├── graphql_queries.py
│ │ │ │ ├── operations.py
│ │ │ │ ├── server_api.py
│ │ │ │ ├── utils.py
│ │ │ │ └── version.py
│ │ │ ├── capture.py
│ │ │ ├── pysync.py
│ │ │ ├── qargparse.py
│ │ │ └── scriptsmenu/
│ │ │ ├── __init__.py
│ │ │ ├── action.py
│ │ │ ├── launchformari.py
│ │ │ ├── launchformaya.py
│ │ │ ├── launchfornuke.py
│ │ │ ├── scriptsmenu.py
│ │ │ └── version.py
│ │ ├── python_2/
│ │ │ ├── README.md
│ │ │ ├── arrow/
│ │ │ │ ├── __init__.py
│ │ │ │ ├── _version.py
│ │ │ │ ├── api.py
│ │ │ │ ├── arrow.py
│ │ │ │ ├── constants.py
│ │ │ │ ├── factory.py
│ │ │ │ ├── formatter.py
│ │ │ │ ├── locales.py
│ │ │ │ ├── parser.py
│ │ │ │ └── util.py
│ │ │ ├── attr/
│ │ │ │ ├── __init__.py
│ │ │ │ ├── __init__.pyi
│ │ │ │ ├── _cmp.py
│ │ │ │ ├── _cmp.pyi
│ │ │ │ ├── _compat.py
│ │ │ │ ├── _config.py
│ │ │ │ ├── _funcs.py
│ │ │ │ ├── _make.py
│ │ │ │ ├── _next_gen.py
│ │ │ │ ├── _version_info.py
│ │ │ │ ├── _version_info.pyi
│ │ │ │ ├── converters.py
│ │ │ │ ├── converters.pyi
│ │ │ │ ├── exceptions.py
│ │ │ │ ├── exceptions.pyi
│ │ │ │ ├── filters.py
│ │ │ │ ├── filters.pyi
│ │ │ │ ├── py.typed
│ │ │ │ ├── setters.py
│ │ │ │ ├── setters.pyi
│ │ │ │ ├── validators.py
│ │ │ │ └── validators.pyi
│ │ │ ├── attrs/
│ │ │ │ ├── __init__.py
│ │ │ │ ├── __init__.pyi
│ │ │ │ ├── converters.py
│ │ │ │ ├── exceptions.py
│ │ │ │ ├── filters.py
│ │ │ │ ├── py.typed
│ │ │ │ ├── setters.py
│ │ │ │ └── validators.py
│ │ │ ├── backports/
│ │ │ │ ├── __init__.py
│ │ │ │ ├── configparser/
│ │ │ │ │ ├── __init__.py
│ │ │ │ │ └── helpers.py
│ │ │ │ └── functools_lru_cache.py
│ │ │ ├── builtins/
│ │ │ │ └── __init__.py
│ │ │ ├── certifi/
│ │ │ │ ├── __init__.py
│ │ │ │ ├── __main__.py
│ │ │ │ ├── cacert.pem
│ │ │ │ └── core.py
│ │ │ ├── chardet/
│ │ │ │ ├── __init__.py
│ │ │ │ ├── big5freq.py
│ │ │ │ ├── big5prober.py
│ │ │ │ ├── chardistribution.py
│ │ │ │ ├── charsetgroupprober.py
│ │ │ │ ├── charsetprober.py
│ │ │ │ ├── cli/
│ │ │ │ │ ├── __init__.py
│ │ │ │ │ └── chardetect.py
│ │ │ │ ├── codingstatemachine.py
│ │ │ │ ├── compat.py
│ │ │ │ ├── cp949prober.py
│ │ │ │ ├── enums.py
│ │ │ │ ├── escprober.py
│ │ │ │ ├── escsm.py
│ │ │ │ ├── eucjpprober.py
│ │ │ │ ├── euckrfreq.py
│ │ │ │ ├── euckrprober.py
│ │ │ │ ├── euctwfreq.py
│ │ │ │ ├── euctwprober.py
│ │ │ │ ├── gb2312freq.py
│ │ │ │ ├── gb2312prober.py
│ │ │ │ ├── hebrewprober.py
│ │ │ │ ├── jisfreq.py
│ │ │ │ ├── jpcntx.py
│ │ │ │ ├── langbulgarianmodel.py
│ │ │ │ ├── langgreekmodel.py
│ │ │ │ ├── langhebrewmodel.py
│ │ │ │ ├── langhungarianmodel.py
│ │ │ │ ├── langrussianmodel.py
│ │ │ │ ├── langthaimodel.py
│ │ │ │ ├── langturkishmodel.py
│ │ │ │ ├── latin1prober.py
│ │ │ │ ├── mbcharsetprober.py
│ │ │ │ ├── mbcsgroupprober.py
│ │ │ │ ├── mbcssm.py
│ │ │ │ ├── metadata/
│ │ │ │ │ ├── __init__.py
│ │ │ │ │ └── languages.py
│ │ │ │ ├── sbcharsetprober.py
│ │ │ │ ├── sbcsgroupprober.py
│ │ │ │ ├── sjisprober.py
│ │ │ │ ├── universaldetector.py
│ │ │ │ ├── utf8prober.py
│ │ │ │ └── version.py
│ │ │ ├── charset_normalizer.py
│ │ │ ├── click/
│ │ │ │ ├── __init__.py
│ │ │ │ ├── _bashcomplete.py
│ │ │ │ ├── _compat.py
│ │ │ │ ├── _termui_impl.py
│ │ │ │ ├── _textwrap.py
│ │ │ │ ├── _unicodefun.py
│ │ │ │ ├── _winconsole.py
│ │ │ │ ├── core.py
│ │ │ │ ├── decorators.py
│ │ │ │ ├── exceptions.py
│ │ │ │ ├── formatting.py
│ │ │ │ ├── globals.py
│ │ │ │ ├── parser.py
│ │ │ │ ├── termui.py
│ │ │ │ ├── testing.py
│ │ │ │ ├── types.py
│ │ │ │ └── utils.py
│ │ │ ├── dns/
│ │ │ │ ├── __init__.py
│ │ │ │ ├── _compat.py
│ │ │ │ ├── dnssec.py
│ │ │ │ ├── e164.py
│ │ │ │ ├── edns.py
│ │ │ │ ├── entropy.py
│ │ │ │ ├── exception.py
│ │ │ │ ├── flags.py
│ │ │ │ ├── grange.py
│ │ │ │ ├── hash.py
│ │ │ │ ├── inet.py
│ │ │ │ ├── ipv4.py
│ │ │ │ ├── ipv6.py
│ │ │ │ ├── message.py
│ │ │ │ ├── name.py
│ │ │ │ ├── namedict.py
│ │ │ │ ├── node.py
│ │ │ │ ├── opcode.py
│ │ │ │ ├── py.typed
│ │ │ │ ├── query.py
│ │ │ │ ├── rcode.py
│ │ │ │ ├── rdata.py
│ │ │ │ ├── rdataclass.py
│ │ │ │ ├── rdataset.py
│ │ │ │ ├── rdatatype.py
│ │ │ │ ├── rdtypes/
│ │ │ │ │ ├── ANY/
│ │ │ │ │ │ ├── AFSDB.py
│ │ │ │ │ │ ├── AVC.py
│ │ │ │ │ │ ├── CAA.py
│ │ │ │ │ │ ├── CDNSKEY.py
│ │ │ │ │ │ ├── CDS.py
│ │ │ │ │ │ ├── CERT.py
│ │ │ │ │ │ ├── CNAME.py
│ │ │ │ │ │ ├── CSYNC.py
│ │ │ │ │ │ ├── DLV.py
│ │ │ │ │ │ ├── DNAME.py
│ │ │ │ │ │ ├── DNSKEY.py
│ │ │ │ │ │ ├── DS.py
│ │ │ │ │ │ ├── EUI48.py
│ │ │ │ │ │ ├── EUI64.py
│ │ │ │ │ │ ├── GPOS.py
│ │ │ │ │ │ ├── HINFO.py
│ │ │ │ │ │ ├── HIP.py
│ │ │ │ │ │ ├── ISDN.py
│ │ │ │ │ │ ├── LOC.py
│ │ │ │ │ │ ├── MX.py
│ │ │ │ │ │ ├── NS.py
│ │ │ │ │ │ ├── NSEC.py
│ │ │ │ │ │ ├── NSEC3.py
│ │ │ │ │ │ ├── NSEC3PARAM.py
│ │ │ │ │ │ ├── OPENPGPKEY.py
│ │ │ │ │ │ ├── PTR.py
│ │ │ │ │ │ ├── RP.py
│ │ │ │ │ │ ├── RRSIG.py
│ │ │ │ │ │ ├── RT.py
│ │ │ │ │ │ ├── SOA.py
│ │ │ │ │ │ ├── SPF.py
│ │ │ │ │ │ ├── SSHFP.py
│ │ │ │ │ │ ├── TLSA.py
│ │ │ │ │ │ ├── TXT.py
│ │ │ │ │ │ ├── URI.py
│ │ │ │ │ │ ├── X25.py
│ │ │ │ │ │ └── __init__.py
│ │ │ │ │ ├── CH/
│ │ │ │ │ │ ├── A.py
│ │ │ │ │ │ └── __init__.py
│ │ │ │ │ ├── IN/
│ │ │ │ │ │ ├── A.py
│ │ │ │ │ │ ├── AAAA.py
│ │ │ │ │ │ ├── APL.py
│ │ │ │ │ │ ├── DHCID.py
│ │ │ │ │ │ ├── IPSECKEY.py
│ │ │ │ │ │ ├── KX.py
│ │ │ │ │ │ ├── NAPTR.py
│ │ │ │ │ │ ├── NSAP.py
│ │ │ │ │ │ ├── NSAP_PTR.py
│ │ │ │ │ │ ├── PX.py
│ │ │ │ │ │ ├── SRV.py
│ │ │ │ │ │ ├── WKS.py
│ │ │ │ │ │ └── __init__.py
│ │ │ │ │ ├── __init__.py
│ │ │ │ │ ├── dnskeybase.py
│ │ │ │ │ ├── dsbase.py
│ │ │ │ │ ├── euibase.py
│ │ │ │ │ ├── mxbase.py
│ │ │ │ │ ├── nsbase.py
│ │ │ │ │ └── txtbase.py
│ │ │ │ ├── renderer.py
│ │ │ │ ├── resolver.py
│ │ │ │ ├── reversename.py
│ │ │ │ ├── rrset.py
│ │ │ │ ├── set.py
│ │ │ │ ├── tokenizer.py
│ │ │ │ ├── tsig.py
│ │ │ │ ├── tsigkeyring.py
│ │ │ │ ├── ttl.py
│ │ │ │ ├── update.py
│ │ │ │ ├── version.py
│ │ │ │ ├── wiredata.py
│ │ │ │ └── zone.py
│ │ │ ├── engineio/
│ │ │ │ ├── __init__.py
│ │ │ │ ├── async_aiohttp.py
│ │ │ │ ├── async_asgi.py
│ │ │ │ ├── async_drivers/
│ │ │ │ │ ├── __init__.py
│ │ │ │ │ ├── aiohttp.py
│ │ │ │ │ ├── asgi.py
│ │ │ │ │ ├── eventlet.py
│ │ │ │ │ ├── gevent.py
│ │ │ │ │ ├── gevent_uwsgi.py
│ │ │ │ │ ├── sanic.py
│ │ │ │ │ ├── threading.py
│ │ │ │ │ └── tornado.py
│ │ │ │ ├── async_eventlet.py
│ │ │ │ ├── async_gevent.py
│ │ │ │ ├── async_gevent_uwsgi.py
│ │ │ │ ├── async_sanic.py
│ │ │ │ ├── async_threading.py
│ │ │ │ ├── async_tornado.py
│ │ │ │ ├── asyncio_client.py
│ │ │ │ ├── asyncio_server.py
│ │ │ │ ├── asyncio_socket.py
│ │ │ │ ├── client.py
│ │ │ │ ├── exceptions.py
│ │ │ │ ├── middleware.py
│ │ │ │ ├── packet.py
│ │ │ │ ├── payload.py
│ │ │ │ ├── server.py
│ │ │ │ ├── socket.py
│ │ │ │ └── static_files.py
│ │ │ ├── functools32/
│ │ │ │ ├── __init__.py
│ │ │ │ ├── _dummy_thread32.py
│ │ │ │ ├── functools32.py
│ │ │ │ └── reprlib32.py
│ │ │ ├── idna/
│ │ │ │ ├── __init__.py
│ │ │ │ ├── codec.py
│ │ │ │ ├── compat.py
│ │ │ │ ├── core.py
│ │ │ │ ├── idnadata.py
│ │ │ │ ├── intranges.py
│ │ │ │ ├── package_data.py
│ │ │ │ └── uts46data.py
│ │ │ ├── opentimelineio/
│ │ │ │ ├── __init__.py
│ │ │ │ ├── adapters/
│ │ │ │ │ ├── __init__.py
│ │ │ │ │ ├── adapter.py
│ │ │ │ │ ├── builtin_adapters.plugin_manifest.json
│ │ │ │ │ ├── cmx_3600.py
│ │ │ │ │ ├── fcp_xml.py
│ │ │ │ │ └── otio_json.py
│ │ │ │ ├── algorithms/
│ │ │ │ │ ├── __init__.py
│ │ │ │ │ ├── filter.py
│ │ │ │ │ ├── stack_algo.py
│ │ │ │ │ ├── timeline_algo.py
│ │ │ │ │ └── track_algo.py
│ │ │ │ ├── console/
│ │ │ │ │ ├── __init__.py
│ │ │ │ │ ├── autogen_serialized_datamodel.py
│ │ │ │ │ ├── console_utils.py
│ │ │ │ │ ├── otiocat.py
│ │ │ │ │ ├── otioconvert.py
│ │ │ │ │ └── otiostat.py
│ │ │ │ ├── core/
│ │ │ │ │ ├── __init__.py
│ │ │ │ │ ├── composable.py
│ │ │ │ │ ├── composition.py
│ │ │ │ │ ├── item.py
│ │ │ │ │ ├── json_serializer.py
│ │ │ │ │ ├── media_reference.py
│ │ │ │ │ ├── serializable_object.py
│ │ │ │ │ ├── type_registry.py
│ │ │ │ │ └── unknown_schema.py
│ │ │ │ ├── exceptions.py
│ │ │ │ ├── hooks.py
│ │ │ │ ├── media_linker.py
│ │ │ │ ├── opentime.py
│ │ │ │ ├── plugins/
│ │ │ │ │ ├── __init__.py
│ │ │ │ │ ├── manifest.py
│ │ │ │ │ └── python_plugin.py
│ │ │ │ ├── schema/
│ │ │ │ │ ├── __init__.py
│ │ │ │ │ ├── clip.py
│ │ │ │ │ ├── effect.py
│ │ │ │ │ ├── external_reference.py
│ │ │ │ │ ├── gap.py
│ │ │ │ │ ├── generator_reference.py
│ │ │ │ │ ├── marker.py
│ │ │ │ │ ├── missing_reference.py
│ │ │ │ │ ├── schemadef.py
│ │ │ │ │ ├── serializable_collection.py
│ │ │ │ │ ├── stack.py
│ │ │ │ │ ├── timeline.py
│ │ │ │ │ ├── track.py
│ │ │ │ │ └── transition.py
│ │ │ │ ├── schemadef/
│ │ │ │ │ └── __init__.py
│ │ │ │ └── test_utils.py
│ │ │ ├── opentimelineio_contrib/
│ │ │ │ ├── __init__.py
│ │ │ │ └── adapters/
│ │ │ │ ├── __init__.py
│ │ │ │ ├── aaf_adapter/
│ │ │ │ │ ├── __init__.py
│ │ │ │ │ └── aaf_writer.py
│ │ │ │ ├── advanced_authoring_format.py
│ │ │ │ ├── ale.py
│ │ │ │ ├── burnins.py
│ │ │ │ ├── contrib_adapters.plugin_manifest.json
│ │ │ │ ├── extern_maya_sequencer.py
│ │ │ │ ├── extern_rv.py
│ │ │ │ ├── fcpx_xml.py
│ │ │ │ ├── ffmpeg_burnins.py
│ │ │ │ ├── hls_playlist.py
│ │ │ │ ├── maya_sequencer.py
│ │ │ │ ├── rv.py
│ │ │ │ └── xges.py
│ │ │ ├── pkg_resources/
│ │ │ │ ├── __init__.py
│ │ │ │ ├── _vendor/
│ │ │ │ │ ├── __init__.py
│ │ │ │ │ ├── appdirs.py
│ │ │ │ │ ├── packaging/
│ │ │ │ │ │ ├── __about__.py
│ │ │ │ │ │ ├── __init__.py
│ │ │ │ │ │ ├── _compat.py
│ │ │ │ │ │ ├── _structures.py
│ │ │ │ │ │ ├── markers.py
│ │ │ │ │ │ ├── requirements.py
│ │ │ │ │ │ ├── specifiers.py
│ │ │ │ │ │ ├── utils.py
│ │ │ │ │ │ └── version.py
│ │ │ │ │ ├── pyparsing.py
│ │ │ │ │ └── six.py
│ │ │ │ ├── extern/
│ │ │ │ │ └── __init__.py
│ │ │ │ ├── py2_warn.py
│ │ │ │ └── py31compat.py
│ │ │ ├── qtpy/
│ │ │ │ ├── Qt3DAnimation.py
│ │ │ │ ├── Qt3DCore.py
│ │ │ │ ├── Qt3DExtras.py
│ │ │ │ ├── Qt3DInput.py
│ │ │ │ ├── Qt3DLogic.py
│ │ │ │ ├── Qt3DRender.py
│ │ │ │ ├── QtCharts.py
│ │ │ │ ├── QtCore.py
│ │ │ │ ├── QtDataVisualization.py
│ │ │ │ ├── QtDesigner.py
│ │ │ │ ├── QtGui.py
│ │ │ │ ├── QtHelp.py
│ │ │ │ ├── QtLocation.py
│ │ │ │ ├── QtMultimedia.py
│ │ │ │ ├── QtMultimediaWidgets.py
│ │ │ │ ├── QtNetwork.py
│ │ │ │ ├── QtOpenGL.py
│ │ │ │ ├── QtPositioning.py
│ │ │ │ ├── QtPrintSupport.py
│ │ │ │ ├── QtQml.py
│ │ │ │ ├── QtQuick.py
│ │ │ │ ├── QtQuickWidgets.py
│ │ │ │ ├── QtSerialPort.py
│ │ │ │ ├── QtSql.py
│ │ │ │ ├── QtSvg.py
│ │ │ │ ├── QtTest.py
│ │ │ │ ├── QtWebChannel.py
│ │ │ │ ├── QtWebEngineWidgets.py
│ │ │ │ ├── QtWebSockets.py
│ │ │ │ ├── QtWidgets.py
│ │ │ │ ├── QtWinExtras.py
│ │ │ │ ├── QtXmlPatterns.py
│ │ │ │ ├── __init__.py
│ │ │ │ ├── _patch/
│ │ │ │ │ ├── __init__.py
│ │ │ │ │ ├── qcombobox.py
│ │ │ │ │ └── qheaderview.py
│ │ │ │ ├── _version.py
│ │ │ │ ├── compat.py
│ │ │ │ ├── py3compat.py
│ │ │ │ ├── tests/
│ │ │ │ │ ├── __init__.py
│ │ │ │ │ ├── conftest.py
│ │ │ │ │ ├── runtests.py
│ │ │ │ │ ├── test_macos_checks.py
│ │ │ │ │ ├── test_main.py
│ │ │ │ │ ├── test_patch_qcombobox.py
│ │ │ │ │ ├── test_patch_qheaderview.py
│ │ │ │ │ ├── test_qdesktopservice_split.py
│ │ │ │ │ ├── test_qt3danimation.py
│ │ │ │ │ ├── test_qt3dcore.py
│ │ │ │ │ ├── test_qt3dextras.py
│ │ │ │ │ ├── test_qt3dinput.py
│ │ │ │ │ ├── test_qt3dlogic.py
│ │ │ │ │ ├── test_qt3drender.py
│ │ │ │ │ ├── test_qtcharts.py
│ │ │ │ │ ├── test_qtcore.py
│ │ │ │ │ ├── test_qtdatavisualization.py
│ │ │ │ │ ├── test_qtdesigner.py
│ │ │ │ │ ├── test_qthelp.py
│ │ │ │ │ ├── test_qtlocation.py
│ │ │ │ │ ├── test_qtmultimedia.py
│ │ │ │ │ ├── test_qtmultimediawidgets.py
│ │ │ │ │ ├── test_qtnetwork.py
│ │ │ │ │ ├── test_qtpositioning.py
│ │ │ │ │ ├── test_qtprintsupport.py
│ │ │ │ │ ├── test_qtqml.py
│ │ │ │ │ ├── test_qtquick.py
│ │ │ │ │ ├── test_qtquickwidgets.py
│ │ │ │ │ ├── test_qtserialport.py
│ │ │ │ │ ├── test_qtsql.py
│ │ │ │ │ ├── test_qtsvg.py
│ │ │ │ │ ├── test_qttest.py
│ │ │ │ │ ├── test_qtwebchannel.py
│ │ │ │ │ ├── test_qtwebenginewidgets.py
│ │ │ │ │ ├── test_qtwebsockets.py
│ │ │ │ │ ├── test_qtwinextras.py
│ │ │ │ │ ├── test_qtxmlpatterns.py
│ │ │ │ │ └── test_uic.py
│ │ │ │ └── uic.py
│ │ │ ├── requests/
│ │ │ │ ├── __init__.py
│ │ │ │ ├── __version__.py
│ │ │ │ ├── _internal_utils.py
│ │ │ │ ├── adapters.py
│ │ │ │ ├── api.py
│ │ │ │ ├── auth.py
│ │ │ │ ├── certs.py
│ │ │ │ ├── compat.py
│ │ │ │ ├── cookies.py
│ │ │ │ ├── exceptions.py
│ │ │ │ ├── help.py
│ │ │ │ ├── hooks.py
│ │ │ │ ├── models.py
│ │ │ │ ├── packages.py
│ │ │ │ ├── sessions.py
│ │ │ │ ├── status_codes.py
│ │ │ │ ├── structures.py
│ │ │ │ └── utils.py
│ │ │ ├── secrets/
│ │ │ │ ├── LICENSE
│ │ │ │ ├── __init__.py
│ │ │ │ └── secrets.py
│ │ │ ├── setuptools/
│ │ │ │ ├── __init__.py
│ │ │ │ ├── _deprecation_warning.py
│ │ │ │ ├── _imp.py
│ │ │ │ ├── _vendor/
│ │ │ │ │ ├── __init__.py
│ │ │ │ │ ├── ordered_set.py
│ │ │ │ │ ├── packaging/
│ │ │ │ │ │ ├── __about__.py
│ │ │ │ │ │ ├── __init__.py
│ │ │ │ │ │ ├── _compat.py
│ │ │ │ │ │ ├── _structures.py
│ │ │ │ │ │ ├── markers.py
│ │ │ │ │ │ ├── requirements.py
│ │ │ │ │ │ ├── specifiers.py
│ │ │ │ │ │ ├── tags.py
│ │ │ │ │ │ ├── utils.py
│ │ │ │ │ │ └── version.py
│ │ │ │ │ ├── pyparsing.py
│ │ │ │ │ └── six.py
│ │ │ │ ├── archive_util.py
│ │ │ │ ├── build_meta.py
│ │ │ │ ├── command/
│ │ │ │ │ ├── __init__.py
│ │ │ │ │ ├── alias.py
│ │ │ │ │ ├── bdist_egg.py
│ │ │ │ │ ├── bdist_rpm.py
│ │ │ │ │ ├── bdist_wininst.py
│ │ │ │ │ ├── build_clib.py
│ │ │ │ │ ├── build_ext.py
│ │ │ │ │ ├── build_py.py
│ │ │ │ │ ├── develop.py
│ │ │ │ │ ├── dist_info.py
│ │ │ │ │ ├── easy_install.py
│ │ │ │ │ ├── egg_info.py
│ │ │ │ │ ├── install.py
│ │ │ │ │ ├── install_egg_info.py
│ │ │ │ │ ├── install_lib.py
│ │ │ │ │ ├── install_scripts.py
│ │ │ │ │ ├── launcher manifest.xml
│ │ │ │ │ ├── py36compat.py
│ │ │ │ │ ├── register.py
│ │ │ │ │ ├── rotate.py
│ │ │ │ │ ├── saveopts.py
│ │ │ │ │ ├── sdist.py
│ │ │ │ │ ├── setopt.py
│ │ │ │ │ ├── test.py
│ │ │ │ │ ├── upload.py
│ │ │ │ │ └── upload_docs.py
│ │ │ │ ├── config.py
│ │ │ │ ├── dep_util.py
│ │ │ │ ├── depends.py
│ │ │ │ ├── dist.py
│ │ │ │ ├── errors.py
│ │ │ │ ├── extension.py
│ │ │ │ ├── extern/
│ │ │ │ │ └── __init__.py
│ │ │ │ ├── glob.py
│ │ │ │ ├── installer.py
│ │ │ │ ├── launch.py
│ │ │ │ ├── lib2to3_ex.py
│ │ │ │ ├── monkey.py
│ │ │ │ ├── msvc.py
│ │ │ │ ├── namespaces.py
│ │ │ │ ├── package_index.py
│ │ │ │ ├── py27compat.py
│ │ │ │ ├── py31compat.py
│ │ │ │ ├── py33compat.py
│ │ │ │ ├── py34compat.py
│ │ │ │ ├── sandbox.py
│ │ │ │ ├── script (dev).tmpl
│ │ │ │ ├── script.tmpl
│ │ │ │ ├── site-patch.py
│ │ │ │ ├── ssl_support.py
│ │ │ │ ├── unicode_utils.py
│ │ │ │ ├── version.py
│ │ │ │ ├── wheel.py
│ │ │ │ └── windows_support.py
│ │ │ ├── setuptools-45.0.0.dist-info/
│ │ │ │ ├── INSTALLER
│ │ │ │ ├── LICENSE
│ │ │ │ ├── METADATA
│ │ │ │ ├── RECORD
│ │ │ │ ├── REQUESTED
│ │ │ │ ├── WHEEL
│ │ │ │ ├── dependency_links.txt
│ │ │ │ ├── entry_points.txt
│ │ │ │ ├── top_level.txt
│ │ │ │ └── zip-safe
│ │ │ ├── socketio/
│ │ │ │ ├── __init__.py
│ │ │ │ ├── asgi.py
│ │ │ │ ├── asyncio_client.py
│ │ │ │ ├── asyncio_manager.py
│ │ │ │ ├── asyncio_namespace.py
│ │ │ │ ├── asyncio_pubsub_manager.py
│ │ │ │ ├── asyncio_redis_manager.py
│ │ │ │ ├── asyncio_server.py
│ │ │ │ ├── base_manager.py
│ │ │ │ ├── client.py
│ │ │ │ ├── exceptions.py
│ │ │ │ ├── kombu_manager.py
│ │ │ │ ├── middleware.py
│ │ │ │ ├── namespace.py
│ │ │ │ ├── packet.py
│ │ │ │ ├── pubsub_manager.py
│ │ │ │ ├── redis_manager.py
│ │ │ │ ├── server.py
│ │ │ │ ├── tornado.py
│ │ │ │ └── zmq_manager.py
│ │ │ ├── urllib3/
│ │ │ │ ├── __init__.py
│ │ │ │ ├── _collections.py
│ │ │ │ ├── _version.py
│ │ │ │ ├── connection.py
│ │ │ │ ├── connectionpool.py
│ │ │ │ ├── contrib/
│ │ │ │ │ ├── __init__.py
│ │ │ │ │ ├── _appengine_environ.py
│ │ │ │ │ ├── _securetransport/
│ │ │ │ │ │ ├── __init__.py
│ │ │ │ │ │ ├── bindings.py
│ │ │ │ │ │ └── low_level.py
│ │ │ │ │ ├── appengine.py
│ │ │ │ │ ├── ntlmpool.py
│ │ │ │ │ ├── pyopenssl.py
│ │ │ │ │ ├── securetransport.py
│ │ │ │ │ └── socks.py
│ │ │ │ ├── exceptions.py
│ │ │ │ ├── fields.py
│ │ │ │ ├── filepost.py
│ │ │ │ ├── packages/
│ │ │ │ │ ├── __init__.py
│ │ │ │ │ ├── backports/
│ │ │ │ │ │ ├── __init__.py
│ │ │ │ │ │ └── makefile.py
│ │ │ │ │ └── six.py
│ │ │ │ ├── poolmanager.py
│ │ │ │ ├── request.py
│ │ │ │ ├── response.py
│ │ │ │ └── util/
│ │ │ │ ├── __init__.py
│ │ │ │ ├── connection.py
│ │ │ │ ├── proxy.py
│ │ │ │ ├── queue.py
│ │ │ │ ├── request.py
│ │ │ │ ├── response.py
│ │ │ │ ├── retry.py
│ │ │ │ ├── ssl_.py
│ │ │ │ ├── ssl_match_hostname.py
│ │ │ │ ├── ssltransport.py
│ │ │ │ ├── timeout.py
│ │ │ │ ├── url.py
│ │ │ │ └── wait.py
│ │ │ └── websocket/
│ │ │ ├── __init__.py
│ │ │ ├── _abnf.py
│ │ │ ├── _app.py
│ │ │ ├── _cookiejar.py
│ │ │ ├── _core.py
│ │ │ ├── _exceptions.py
│ │ │ ├── _handshake.py
│ │ │ ├── _http.py
│ │ │ ├── _logging.py
│ │ │ ├── _socket.py
│ │ │ ├── _ssl_compat.py
│ │ │ ├── _url.py
│ │ │ ├── _utils.py
│ │ │ └── tests/
│ │ │ ├── __init__.py
│ │ │ ├── data/
│ │ │ │ ├── header01.txt
│ │ │ │ ├── header02.txt
│ │ │ │ └── header03.txt
│ │ │ ├── test_abnf.py
│ │ │ ├── test_app.py
│ │ │ ├── test_cookiejar.py
│ │ │ ├── test_http.py
│ │ │ ├── test_url.py
│ │ │ └── test_websocket.py
│ │ └── python_3/
│ │ └── README.md
│ ├── version.py
│ └── widgets/
│ ├── README.md
│ ├── __init__.py
│ ├── color_widgets/
│ │ ├── __init__.py
│ │ ├── color_inputs.py
│ │ ├── color_picker_widget.py
│ │ ├── color_screen_pick.py
│ │ ├── color_triangle.py
│ │ └── color_view.py
│ ├── message_window.py
│ ├── nice_checkbox.py
│ ├── password_dialog.py
│ ├── popup.py
│ └── sliders.py
├── poetry.toml
├── pyproject.toml
├── server_addon/
│ ├── README.md
│ ├── aftereffects/
│ │ ├── LICENSE
│ │ ├── README.md
│ │ └── server/
│ │ ├── __init__.py
│ │ ├── settings/
│ │ │ ├── __init__.py
│ │ │ ├── creator_plugins.py
│ │ │ ├── imageio.py
│ │ │ ├── main.py
│ │ │ ├── publish_plugins.py
│ │ │ ├── templated_workfile_build.py
│ │ │ └── workfile_builder.py
│ │ └── version.py
│ ├── applications/
│ │ └── server/
│ │ ├── __init__.py
│ │ ├── applications.json
│ │ ├── settings.py
│ │ ├── tools.json
│ │ └── version.py
│ ├── blender/
│ │ └── server/
│ │ ├── __init__.py
│ │ ├── settings/
│ │ │ ├── __init__.py
│ │ │ ├── imageio.py
│ │ │ ├── main.py
│ │ │ ├── publish_plugins.py
│ │ │ └── render_settings.py
│ │ └── version.py
│ ├── celaction/
│ │ └── server/
│ │ ├── __init__.py
│ │ ├── imageio.py
│ │ ├── settings.py
│ │ └── version.py
│ ├── clockify/
│ │ └── server/
│ │ ├── __init__.py
│ │ ├── settings.py
│ │ └── version.py
│ ├── core/
│ │ └── server/
│ │ ├── __init__.py
│ │ ├── settings/
│ │ │ ├── __init__.py
│ │ │ ├── main.py
│ │ │ ├── publish_plugins.py
│ │ │ └── tools.py
│ │ └── version.py
│ ├── create_ayon_addons.py
│ ├── deadline/
│ │ └── server/
│ │ ├── __init__.py
│ │ ├── settings/
│ │ │ ├── __init__.py
│ │ │ ├── main.py
│ │ │ └── publish_plugins.py
│ │ └── version.py
│ ├── equalizer/
│ │ ├── LICENSE
│ │ ├── README.md
│ │ └── server/
│ │ ├── __init__.py
│ │ ├── settings/
│ │ │ ├── __init__.py
│ │ │ ├── creator_plugins.py
│ │ │ └── main.py
│ │ └── version.py
│ ├── flame/
│ │ └── server/
│ │ ├── __init__.py
│ │ ├── settings/
│ │ │ ├── __init__.py
│ │ │ ├── create_plugins.py
│ │ │ ├── imageio.py
│ │ │ ├── loader_plugins.py
│ │ │ ├── main.py
│ │ │ └── publish_plugins.py
│ │ └── version.py
│ ├── fusion/
│ │ └── server/
│ │ ├── __init__.py
│ │ ├── imageio.py
│ │ ├── settings.py
│ │ └── version.py
│ ├── harmony/
│ │ ├── LICENSE
│ │ ├── README.md
│ │ └── server/
│ │ ├── __init__.py
│ │ ├── settings/
│ │ │ ├── __init__.py
│ │ │ ├── imageio.py
│ │ │ ├── main.py
│ │ │ └── publish_plugins.py
│ │ └── version.py
│ ├── hiero/
│ │ └── server/
│ │ ├── __init__.py
│ │ ├── settings/
│ │ │ ├── __init__.py
│ │ │ ├── common.py
│ │ │ ├── create_plugins.py
│ │ │ ├── filters.py
│ │ │ ├── imageio.py
│ │ │ ├── loader_plugins.py
│ │ │ ├── main.py
│ │ │ ├── publish_plugins.py
│ │ │ └── scriptsmenu.py
│ │ └── version.py
│ ├── houdini/
│ │ └── server/
│ │ ├── __init__.py
│ │ ├── settings/
│ │ │ ├── __init__.py
│ │ │ ├── create.py
│ │ │ ├── general.py
│ │ │ ├── imageio.py
│ │ │ ├── main.py
│ │ │ ├── publish.py
│ │ │ └── shelves.py
│ │ └── version.py
│ ├── max/
│ │ └── server/
│ │ ├── __init__.py
│ │ ├── settings/
│ │ │ ├── __init__.py
│ │ │ ├── create_review_settings.py
│ │ │ ├── imageio.py
│ │ │ ├── main.py
│ │ │ ├── publishers.py
│ │ │ └── render_settings.py
│ │ └── version.py
│ ├── maya/
│ │ ├── LICENCE
│ │ ├── README.md
│ │ └── server/
│ │ ├── __init__.py
│ │ ├── settings/
│ │ │ ├── __init__.py
│ │ │ ├── creators.py
│ │ │ ├── explicit_plugins_loading.py
│ │ │ ├── imageio.py
│ │ │ ├── include_handles.py
│ │ │ ├── loaders.py
│ │ │ ├── main.py
│ │ │ ├── maya_dirmap.py
│ │ │ ├── publish_playblast.py
│ │ │ ├── publishers.py
│ │ │ ├── render_settings.py
│ │ │ ├── scriptsmenu.py
│ │ │ ├── templated_workfile_settings.py
│ │ │ └── workfile_build_settings.py
│ │ └── version.py
│ ├── nuke/
│ │ └── server/
│ │ ├── __init__.py
│ │ ├── settings/
│ │ │ ├── __init__.py
│ │ │ ├── common.py
│ │ │ ├── create_plugins.py
│ │ │ ├── dirmap.py
│ │ │ ├── general.py
│ │ │ ├── gizmo.py
│ │ │ ├── imageio.py
│ │ │ ├── loader_plugins.py
│ │ │ ├── main.py
│ │ │ ├── publish_plugins.py
│ │ │ ├── scriptsmenu.py
│ │ │ ├── templated_workfile_build.py
│ │ │ └── workfile_builder.py
│ │ └── version.py
│ ├── openpype/
│ │ ├── client/
│ │ │ └── pyproject.toml
│ │ └── server/
│ │ └── __init__.py
│ ├── photoshop/
│ │ ├── LICENSE
│ │ ├── README.md
│ │ └── server/
│ │ ├── __init__.py
│ │ ├── settings/
│ │ │ ├── __init__.py
│ │ │ ├── creator_plugins.py
│ │ │ ├── imageio.py
│ │ │ ├── main.py
│ │ │ ├── publish_plugins.py
│ │ │ └── workfile_builder.py
│ │ └── version.py
│ ├── resolve/
│ │ └── server/
│ │ ├── __init__.py
│ │ ├── imageio.py
│ │ ├── settings.py
│ │ └── version.py
│ ├── royal_render/
│ │ └── server/
│ │ ├── __init__.py
│ │ ├── settings.py
│ │ └── version.py
│ ├── substancepainter/
│ │ └── server/
│ │ ├── __init__.py
│ │ ├── settings/
│ │ │ ├── __init__.py
│ │ │ ├── imageio.py
│ │ │ ├── load_plugins.py
│ │ │ └── main.py
│ │ └── version.py
│ ├── timers_manager/
│ │ └── server/
│ │ ├── __init__.py
│ │ ├── settings.py
│ │ └── version.py
│ ├── traypublisher/
│ │ └── server/
│ │ ├── LICENSE
│ │ ├── README.md
│ │ ├── __init__.py
│ │ ├── settings/
│ │ │ ├── __init__.py
│ │ │ ├── creator_plugins.py
│ │ │ ├── editorial_creators.py
│ │ │ ├── imageio.py
│ │ │ ├── main.py
│ │ │ ├── publish_plugins.py
│ │ │ └── simple_creators.py
│ │ └── version.py
│ ├── tvpaint/
│ │ └── server/
│ │ ├── __init__.py
│ │ ├── settings/
│ │ │ ├── __init__.py
│ │ │ ├── create_plugins.py
│ │ │ ├── filters.py
│ │ │ ├── imageio.py
│ │ │ ├── main.py
│ │ │ ├── publish_plugins.py
│ │ │ └── workfile_builder.py
│ │ └── version.py
│ └── unreal/
│ └── server/
│ ├── __init__.py
│ ├── imageio.py
│ ├── settings.py
│ └── version.py
├── setup.cfg
├── setup.py
├── start.py
├── tests/
│ ├── README.md
│ ├── __init__.py
│ ├── conftest.py
│ ├── integration/
│ │ ├── README.md
│ │ └── hosts/
│ │ ├── aftereffects/
│ │ │ ├── lib.py
│ │ │ ├── test_deadline_publish_in_aftereffects.py
│ │ │ ├── test_deadline_publish_in_aftereffects_multicomposition.py
│ │ │ ├── test_publish_in_aftereffects.py
│ │ │ ├── test_publish_in_aftereffects_legacy.py
│ │ │ └── test_publish_in_aftereffects_multiframe.py
│ │ ├── maya/
│ │ │ ├── lib.py
│ │ │ ├── test_deadline_publish_in_maya/
│ │ │ │ ├── README.md
│ │ │ │ ├── expected/
│ │ │ │ │ └── test_project/
│ │ │ │ │ └── test_asset/
│ │ │ │ │ ├── publish/
│ │ │ │ │ │ ├── model/
│ │ │ │ │ │ │ └── modelMain/
│ │ │ │ │ │ │ ├── hero/
│ │ │ │ │ │ │ │ ├── test_project_test_asset_modelMain_hero.abc
│ │ │ │ │ │ │ │ └── test_project_test_asset_modelMain_hero.ma
│ │ │ │ │ │ │ └── v001/
│ │ │ │ │ │ │ ├── test_project_test_asset_modelMain_v001.abc
│ │ │ │ │ │ │ └── test_project_test_asset_modelMain_v001.ma
│ │ │ │ │ │ ├── render/
│ │ │ │ │ │ │ └── renderTest_taskMain_beauty/
│ │ │ │ │ │ │ └── v001/
│ │ │ │ │ │ │ └── test_project_test_asset_renderTest_taskMain_beauty_v001.exr
│ │ │ │ │ │ └── workfile/
│ │ │ │ │ │ └── workfileTest_task/
│ │ │ │ │ │ └── v001/
│ │ │ │ │ │ └── test_project_test_asset_workfileTest_task_v001.ma
│ │ │ │ │ └── work/
│ │ │ │ │ └── test_task/
│ │ │ │ │ ├── test_project_test_asset_test_task_v001.ma
│ │ │ │ │ ├── test_project_test_asset_test_task_v002.ma
│ │ │ │ │ └── workspace.mel
│ │ │ │ └── input/
│ │ │ │ ├── dumps/
│ │ │ │ │ ├── avalon_tests/
│ │ │ │ │ │ ├── test_project.bson
│ │ │ │ │ │ └── test_project.metadata.json
│ │ │ │ │ └── openpype_tests/
│ │ │ │ │ ├── settings.bson
│ │ │ │ │ └── settings.metadata.json
│ │ │ │ ├── env_vars/
│ │ │ │ │ └── env_var.json
│ │ │ │ ├── startup/
│ │ │ │ │ └── userSetup.py
│ │ │ │ └── workfile/
│ │ │ │ └── test_project_test_asset_test_task_v001.ma
│ │ │ ├── test_deadline_publish_in_maya.py
│ │ │ ├── test_publish_in_maya/
│ │ │ │ ├── expected/
│ │ │ │ │ └── test_project/
│ │ │ │ │ └── test_asset/
│ │ │ │ │ ├── publish/
│ │ │ │ │ │ ├── model/
│ │ │ │ │ │ │ └── modelMain/
│ │ │ │ │ │ │ ├── hero/
│ │ │ │ │ │ │ │ ├── test_project_test_asset_modelMain_hero.abc
│ │ │ │ │ │ │ │ └── test_project_test_asset_modelMain_hero.ma
│ │ │ │ │ │ │ └── v001/
│ │ │ │ │ │ │ ├── test_project_test_asset_modelMain_v001.abc
│ │ │ │ │ │ │ └── test_project_test_asset_modelMain_v001.ma
│ │ │ │ │ │ └── workfile/
│ │ │ │ │ │ └── workfileTest_task/
│ │ │ │ │ │ └── v001/
│ │ │ │ │ │ └── test_project_test_asset_workfileTest_task_v001.ma
│ │ │ │ │ └── work/
│ │ │ │ │ └── test_task/
│ │ │ │ │ ├── test_project_test_asset_test_task_v001.ma
│ │ │ │ │ ├── test_project_test_asset_test_task_v002.ma
│ │ │ │ │ └── workspace.mel
│ │ │ │ └── input/
│ │ │ │ ├── dumps/
│ │ │ │ │ ├── avalon_tests/
│ │ │ │ │ │ ├── test_project.bson
│ │ │ │ │ │ └── test_project.metadata.json
│ │ │ │ │ └── openpype_tests/
│ │ │ │ │ ├── settings.bson
│ │ │ │ │ └── settings.metadata.json
│ │ │ │ ├── env_vars/
│ │ │ │ │ └── env_var.json
│ │ │ │ ├── startup/
│ │ │ │ │ └── userSetup.py
│ │ │ │ └── workfile/
│ │ │ │ └── test_project_test_asset_test_task_v001.ma
│ │ │ └── test_publish_in_maya.py
│ │ ├── nuke/
│ │ │ ├── lib.py
│ │ │ ├── test_deadline_publish_in_nuke.py
│ │ │ ├── test_deadline_publish_in_nuke_prerender.py
│ │ │ └── test_publish_in_nuke.py
│ │ └── photoshop/
│ │ ├── lib.py
│ │ ├── test_publish_in_photoshop.py
│ │ ├── test_publish_in_photoshop_auto_image.py
│ │ └── test_publish_in_photoshop_review.py
│ ├── lib/
│ │ ├── README.md
│ │ ├── __init__.py
│ │ ├── assert_classes.py
│ │ ├── db_handler.py
│ │ ├── file_handler.py
│ │ └── testing_classes.py
│ └── unit/
│ ├── igniter/
│ │ ├── test_bootstrap_repos.py
│ │ └── test_tools.py
│ └── openpype/
│ ├── conftest.py
│ ├── hosts/
│ │ ├── photoshop/
│ │ │ └── test_lib.py
│ │ └── unreal/
│ │ └── plugins/
│ │ └── publish/
│ │ └── test_validate_sequence_frames.py
│ ├── lib/
│ │ ├── test_delivery.py
│ │ ├── test_event_system.py
│ │ └── test_user_settings.py
│ ├── modules/
│ │ └── sync_server/
│ │ ├── test_module_api.py
│ │ └── test_site_operations.py
│ ├── pipeline/
│ │ ├── lib.py
│ │ ├── publish/
│ │ │ └── test_publish_plugins.py
│ │ ├── test_colorspace.py
│ │ ├── test_colorspace_convert_colorspace_enumerator_item.py
│ │ └── test_colorspace_get_colorspaces_enumerator_items.py
│ └── plugins/
│ └── publish/
│ └── test_extract_review.py
├── tools/
│ ├── build.ps1
│ ├── build.sh
│ ├── build_dependencies.py
│ ├── build_win_installer.ps1
│ ├── ci_tools.py
│ ├── create_env.ps1
│ ├── create_env.sh
│ ├── create_zip.ps1
│ ├── create_zip.py
│ ├── create_zip.sh
│ ├── docker_build.ps1
│ ├── docker_build.sh
│ ├── fetch_thirdparty_libs.ps1
│ ├── fetch_thirdparty_libs.py
│ ├── fetch_thirdparty_libs.sh
│ ├── get_python_packages_info.py
│ ├── make_docs.ps1
│ ├── make_docs.sh
│ ├── openpype_console.bat
│ ├── pack_project.ps1
│ ├── parse_pyproject.py
│ ├── run_documentation.ps1
│ ├── run_mongo.ps1
│ ├── run_mongo.sh
│ ├── run_project_manager.ps1
│ ├── run_projectmanager.sh
│ ├── run_publish_report_viewer.ps1
│ ├── run_settings.ps1
│ ├── run_settings.sh
│ ├── run_tests.ps1
│ ├── run_tests.sh
│ ├── run_tray.ps1
│ ├── run_tray.sh
│ ├── unpack_project.ps1
│ ├── update_submodules.ps1
│ └── update_submodules.sh
├── vendor/
│ └── README.md
└── website/
├── README.md
├── docs/
│ ├── admin_builds.md
│ ├── admin_distribute.md
│ ├── admin_docsexamples.md
│ ├── admin_environment.md
│ ├── admin_hosts_aftereffects.md
│ ├── admin_hosts_blender.md
│ ├── admin_hosts_harmony.md
│ ├── admin_hosts_hiero.md
│ ├── admin_hosts_houdini.md
│ ├── admin_hosts_maya.md
│ ├── admin_hosts_nuke.md
│ ├── admin_hosts_photoshop.md
│ ├── admin_hosts_resolve.md
│ ├── admin_hosts_tvpaint.md
│ ├── admin_openpype_commands.md
│ ├── admin_releases.md
│ ├── admin_settings.md
│ ├── admin_settings_local.md
│ ├── admin_settings_project_anatomy.md
│ ├── admin_settings_system.md
│ ├── admin_use.md
│ ├── admin_webserver_for_webpublisher.md
│ ├── artist_concepts.md
│ ├── artist_ftrack.md
│ ├── artist_getting_started.md
│ ├── artist_hosts_3dsmax.md
│ ├── artist_hosts_aftereffects.md
│ ├── artist_hosts_blender.md
│ ├── artist_hosts_harmony.md
│ ├── artist_hosts_hiero.md
│ ├── artist_hosts_houdini.md
│ ├── artist_hosts_maya.md
│ ├── artist_hosts_maya_arnold.md
│ ├── artist_hosts_maya_multiverse.md
│ ├── artist_hosts_maya_redshift.md
│ ├── artist_hosts_maya_vray.md
│ ├── artist_hosts_maya_xgen.md
│ ├── artist_hosts_maya_yeti.md
│ ├── artist_hosts_nuke_tut.md
│ ├── artist_hosts_photoshop.md
│ ├── artist_hosts_resolve.md
│ ├── artist_hosts_substancepainter.md
│ ├── artist_hosts_tvpaint.md
│ ├── artist_hosts_unreal.md
│ ├── artist_install.md
│ ├── artist_kitsu.md
│ ├── artist_publish.md
│ ├── artist_tools.md
│ ├── artist_tools_context_manager.md
│ ├── artist_tools_creator.md
│ ├── artist_tools_inventory.md
│ ├── artist_tools_library_loader.md
│ ├── artist_tools_loader.md
│ ├── artist_tools_look_assigner.md
│ ├── artist_tools_publisher.md
│ ├── artist_tools_subset_manager.md
│ ├── artist_tools_sync_queu.md
│ ├── artist_tools_workfiles.md
│ ├── artist_work.md
│ ├── dev_blender.md
│ ├── dev_build.md
│ ├── dev_colorspace.md
│ ├── dev_contribute.md
│ ├── dev_deadline.md
│ ├── dev_host_implementation.md
│ ├── dev_introduction.md
│ ├── dev_publishing.md
│ ├── dev_requirements.md
│ ├── dev_settings.md
│ ├── dev_testing.md
│ ├── features.md
│ ├── manager_ftrack.md
│ ├── manager_ftrack_actions.md
│ ├── module_clockify.md
│ ├── module_deadline.md
│ ├── module_ftrack.md
│ ├── module_kitsu.md
│ ├── module_royalrender.md
│ ├── module_site_sync.md
│ ├── module_slack.md
│ ├── project_settings/
│ │ ├── settings_project_global.md
│ │ ├── settings_project_nuke.md
│ │ └── settings_project_standalone.md
│ ├── pype2/
│ │ ├── admin_anatomy.md
│ │ ├── admin_config.md
│ │ ├── admin_ftrack.md
│ │ ├── admin_hosts.md
│ │ ├── admin_install.md
│ │ ├── admin_introduction.md
│ │ ├── admin_presets_ftrack.md
│ │ ├── admin_presets_maya.md
│ │ ├── admin_presets_nukestudio.md
│ │ ├── admin_presets_plugins.md
│ │ ├── admin_presets_tools.md
│ │ ├── admin_pype_commands.md
│ │ └── admin_setup_troubleshooting.md
│ └── system_introduction.md
├── docusaurus.config.js
├── publish.cmd
├── sidebars.js
├── src/
│ ├── components/
│ │ ├── BadgesSection/
│ │ │ ├── badges.js
│ │ │ ├── index.js
│ │ │ └── styles.module.css
│ │ ├── GithubButton/
│ │ │ └── index.js
│ │ └── index.js
│ ├── css/
│ │ └── custom.css
│ └── pages/
│ ├── features.js
│ ├── index.js
│ └── styles.module.css
└── static/
├── .circleci/
│ └── config.yml
└── img/
└── app_logos.psd
================================================
FILE CONTENTS
================================================
================================================
FILE: .all-contributorsrc
================================================
{
"projectName": "OpenPype",
"projectOwner": "ynput",
"repoType": "github",
"repoHost": "https://github.com",
"files": [
"README.md"
],
"imageSize": 100,
"commit": true,
"commitConvention": "none",
"contributors": [
{
"login": "mkolar",
"name": "Milan Kolar",
"avatar_url": "https://avatars.githubusercontent.com/u/3333008?v=4",
"profile": "http://pype.club/",
"contributions": [
"code",
"doc",
"infra",
"business",
"content",
"fundingFinding",
"maintenance",
"projectManagement",
"review",
"mentoring",
"question"
]
},
{
"login": "jakubjezek001",
"name": "Jakub Ježek",
"avatar_url": "https://avatars.githubusercontent.com/u/40640033?v=4",
"profile": "https://www.linkedin.com/in/jakubjezek79",
"contributions": [
"code",
"doc",
"infra",
"content",
"review",
"maintenance",
"mentoring",
"projectManagement",
"question"
]
},
{
"login": "antirotor",
"name": "Ondřej Samohel",
"avatar_url": "https://avatars.githubusercontent.com/u/33513211?v=4",
"profile": "https://github.com/antirotor",
"contributions": [
"code",
"doc",
"infra",
"content",
"review",
"maintenance",
"mentoring",
"projectManagement",
"question"
]
},
{
"login": "iLLiCiTiT",
"name": "Jakub Trllo",
"avatar_url": "https://avatars.githubusercontent.com/u/43494761?v=4",
"profile": "https://github.com/iLLiCiTiT",
"contributions": [
"code",
"doc",
"infra",
"review",
"maintenance",
"question"
]
},
{
"login": "kalisp",
"name": "Petr Kalis",
"avatar_url": "https://avatars.githubusercontent.com/u/4457962?v=4",
"profile": "https://github.com/kalisp",
"contributions": [
"code",
"doc",
"infra",
"review",
"maintenance",
"question"
]
},
{
"login": "64qam",
"name": "64qam",
"avatar_url": "https://avatars.githubusercontent.com/u/26925793?v=4",
"profile": "https://github.com/64qam",
"contributions": [
"code",
"review",
"doc",
"infra",
"projectManagement",
"maintenance",
"content",
"userTesting"
]
},
{
"login": "BigRoy",
"name": "Roy Nieterau",
"avatar_url": "https://avatars.githubusercontent.com/u/2439881?v=4",
"profile": "http://www.colorbleed.nl/",
"contributions": [
"code",
"doc",
"review",
"mentoring",
"question"
]
},
{
"login": "tokejepsen",
"name": "Toke Jepsen",
"avatar_url": "https://avatars.githubusercontent.com/u/1860085?v=4",
"profile": "https://github.com/tokejepsen",
"contributions": [
"code",
"doc",
"review",
"mentoring",
"question"
]
},
{
"login": "jrsndl",
"name": "Jiri Sindelar",
"avatar_url": "https://avatars.githubusercontent.com/u/45896205?v=4",
"profile": "https://github.com/jrsndl",
"contributions": [
"code",
"review",
"doc",
"content",
"tutorial",
"userTesting"
]
},
{
"login": "simonebarbieri",
"name": "Simone Barbieri",
"avatar_url": "https://avatars.githubusercontent.com/u/1087869?v=4",
"profile": "https://barbierisimone.com/",
"contributions": [
"code",
"doc"
]
},
{
"login": "karimmozilla",
"name": "karimmozilla",
"avatar_url": "https://avatars.githubusercontent.com/u/82811760?v=4",
"profile": "http://karimmozilla.xyz/",
"contributions": [
"code"
]
},
{
"login": "Allan-I",
"name": "Allan I. A.",
"avatar_url": "https://avatars.githubusercontent.com/u/76656700?v=4",
"profile": "https://github.com/Allan-I",
"contributions": [
"code"
]
},
{
"login": "m-u-r-p-h-y",
"name": "murphy",
"avatar_url": "https://avatars.githubusercontent.com/u/352795?v=4",
"profile": "https://www.linkedin.com/in/mmuurrpphhyy/",
"contributions": [
"code",
"review",
"userTesting",
"doc",
"projectManagement"
]
},
{
"login": "aardschok",
"name": "Wijnand Koreman",
"avatar_url": "https://avatars.githubusercontent.com/u/26920875?v=4",
"profile": "https://github.com/aardschok",
"contributions": [
"code"
]
},
{
"login": "zhoub",
"name": "Bo Zhou",
"avatar_url": "https://avatars.githubusercontent.com/u/1798206?v=4",
"profile": "http://jedimaster.cnblogs.com/",
"contributions": [
"code"
]
},
{
"login": "ClementHector",
"name": "Clément Hector",
"avatar_url": "https://avatars.githubusercontent.com/u/7068597?v=4",
"profile": "https://www.linkedin.com/in/clementhector/",
"contributions": [
"code",
"review"
]
},
{
"login": "davidlatwe",
"name": "David Lai",
"avatar_url": "https://avatars.githubusercontent.com/u/3357009?v=4",
"profile": "https://twitter.com/davidlatwe",
"contributions": [
"code",
"review"
]
},
{
"login": "2-REC",
"name": "Derek ",
"avatar_url": "https://avatars.githubusercontent.com/u/42170307?v=4",
"profile": "https://github.com/2-REC",
"contributions": [
"code",
"doc"
]
},
{
"login": "gabormarinov",
"name": "Gábor Marinov",
"avatar_url": "https://avatars.githubusercontent.com/u/8620515?v=4",
"profile": "https://github.com/gabormarinov",
"contributions": [
"code",
"doc"
]
},
{
"login": "icyvapor",
"name": "icyvapor",
"avatar_url": "https://avatars.githubusercontent.com/u/1195278?v=4",
"profile": "https://github.com/icyvapor",
"contributions": [
"code",
"doc"
]
},
{
"login": "jlorrain",
"name": "Jérôme LORRAIN",
"avatar_url": "https://avatars.githubusercontent.com/u/7955673?v=4",
"profile": "https://github.com/jlorrain",
"contributions": [
"code"
]
},
{
"login": "dmo-j-cube",
"name": "David Morris-Oliveros",
"avatar_url": "https://avatars.githubusercontent.com/u/89823400?v=4",
"profile": "https://github.com/dmo-j-cube",
"contributions": [
"code"
]
},
{
"login": "BenoitConnan",
"name": "BenoitConnan",
"avatar_url": "https://avatars.githubusercontent.com/u/82808268?v=4",
"profile": "https://github.com/BenoitConnan",
"contributions": [
"code"
]
},
{
"login": "Malthaldar",
"name": "Malthaldar",
"avatar_url": "https://avatars.githubusercontent.com/u/33671694?v=4",
"profile": "https://github.com/Malthaldar",
"contributions": [
"code"
]
},
{
"login": "svenneve",
"name": "Sven Neve",
"avatar_url": "https://avatars.githubusercontent.com/u/2472863?v=4",
"profile": "http://www.svenneve.com/",
"contributions": [
"code"
]
},
{
"login": "zafrs",
"name": "zafrs",
"avatar_url": "https://avatars.githubusercontent.com/u/26890002?v=4",
"profile": "https://github.com/zafrs",
"contributions": [
"code"
]
},
{
"login": "Tilix4",
"name": "Félix David",
"avatar_url": "https://avatars.githubusercontent.com/u/22875539?v=4",
"profile": "http://felixdavid.com/",
"contributions": [
"code",
"doc"
]
},
{
"login": "movalex",
"name": "Alexey Bogomolov",
"avatar_url": "https://avatars.githubusercontent.com/u/11698866?v=4",
"profile": "http://abogomolov.com",
"contributions": [
"code"
]
}
],
"contributorsPerLine": 7,
"skipCi": true,
"commitType": "docs"
}
================================================
FILE: .dockerignore
================================================
# Created by .ignore support plugin (hsz.mobi)
### Python template
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
cover/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
.pybuilder/
target/
# Jupyter Notebook
.ipynb_checkpoints
# IPython
profile_default/
ipython_config.py
# pyenv
# For a library or package, you might want to ignore these files since the code is
# intended to run in multiple environments; otherwise, check them in:
.python-version
# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that don't work, or not
# install all needed dependencies.
#Pipfile.lock
# PEP 582; used by e.g. github.com/David-OConnor/pyflow
__pypackages__/
# Celery stuff
celerybeat-schedule
celerybeat.pid
# SageMath parsed files
*.sage.py
# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
.dmypy.json
dmypy.json
# Pyre type checker
.pyre/
# pytype static type analyzer
.pytype/
# Cython debug symbols
cython_debug/
.poetry/
.github/
vendor/bin/
vendor/python/
docs/
website/
================================================
FILE: .gitattributes
================================================
* text=auto
*.sh text eol=lf
*.command eol=lf
*.bat text eol=crlf
*.js eol=lf
*.c eol=lf
================================================
FILE: .github/ISSUE_TEMPLATE/bug_report.yml
================================================
name: Bug Report
description: File a bug report
title: 'Bug: '
labels:
- 'type: bug'
body:
- type: markdown
attributes:
value: |
Thanks for taking the time to fill out this bug report!
- type: checkboxes
attributes:
label: Is there an existing issue for this?
description: >-
Please search to see if an issue already exists for the bug you
encountered.
options:
- label: I have searched the existing issues
required: true
- type: textarea
attributes:
label: 'Current Behavior:'
description: A concise description of what you're experiencing.
validations:
required: true
- type: textarea
attributes:
label: 'Expected Behavior:'
description: A concise description of what you expected to happen.
validations:
required: false
- type: dropdown
id: _version
attributes:
label: Version
description: What version are you running? Look to OpenPype Tray
options:
- 3.18.12-nightly.26
- 3.18.12-nightly.25
- 3.18.12-nightly.24
- 3.18.12-nightly.23
- 3.18.12-nightly.22
- 3.18.12-nightly.21
- 3.18.12-nightly.20
- 3.18.12-nightly.19
- 3.18.12-nightly.18
- 3.18.12-nightly.17
- 3.18.12-nightly.16
- 3.18.12-nightly.15
- 3.18.12-nightly.14
- 3.18.12-nightly.13
- 3.18.12-nightly.12
- 3.18.12-nightly.11
- 3.18.12-nightly.10
- 3.18.12-nightly.9
- 3.18.12-nightly.8
- 3.18.12-nightly.7
- 3.18.12-nightly.6
- 3.18.12-nightly.5
- 3.18.12-nightly.4
- 3.18.12-nightly.3
- 3.18.12-nightly.2
- 3.18.12-nightly.1
- 3.18.11
- 3.18.11-nightly.10
- 3.18.11-nightly.9
- 3.18.11-nightly.8
- 3.18.11-nightly.7
- 3.18.11-nightly.6
- 3.18.11-nightly.5
- 3.18.11-nightly.4
- 3.18.11-nightly.3
- 3.18.11-nightly.2
- 3.18.11-nightly.1
- 3.18.10
- 3.18.10-nightly.2
- 3.18.10-nightly.1
- 3.18.9
- 3.18.9-nightly.11
- 3.18.9-nightly.10
- 3.18.9-nightly.9
- 3.18.9-nightly.8
- 3.18.9-nightly.7
- 3.18.9-nightly.6
- 3.18.9-nightly.5
- 3.18.9-nightly.4
- 3.18.9-nightly.3
- 3.18.9-nightly.2
- 3.18.9-nightly.1
- 3.18.8
- 3.18.8-nightly.2
- 3.18.8-nightly.1
- 3.18.7
- 3.18.7-nightly.5
- 3.18.7-nightly.4
- 3.18.7-nightly.3
- 3.18.7-nightly.2
- 3.18.7-nightly.1
- 3.18.6
- 3.18.6-nightly.2
- 3.18.6-nightly.1
- 3.18.5
- 3.18.5-nightly.3
- 3.18.5-nightly.2
- 3.18.5-nightly.1
- 3.18.4
- 3.18.4-nightly.1
- 3.18.3
- 3.18.3-nightly.2
- 3.18.3-nightly.1
- 3.18.2
- 3.18.2-nightly.6
- 3.18.2-nightly.5
- 3.18.2-nightly.4
- 3.18.2-nightly.3
- 3.18.2-nightly.2
- 3.18.2-nightly.1
- 3.18.1
- 3.18.1-nightly.1
- 3.18.0
- 3.17.7
- 3.17.7-nightly.7
- 3.17.7-nightly.6
- 3.17.7-nightly.5
- 3.17.7-nightly.4
- 3.17.7-nightly.3
- 3.17.7-nightly.2
- 3.17.7-nightly.1
- 3.17.6
- 3.17.6-nightly.3
- 3.17.6-nightly.2
- 3.17.6-nightly.1
- 3.17.5
- 3.17.5-nightly.3
- 3.17.5-nightly.2
- 3.17.5-nightly.1
- 3.17.4
validations:
required: true
- type: dropdown
validations:
required: true
attributes:
label: What platform you are running OpenPype on?
description: |
Please specify the operating systems you are running OpenPype with.
multiple: true
options:
- Windows
- Linux / Centos
- Linux / Ubuntu
- Linux / RedHat
- MacOS
- type: textarea
id: to-reproduce
attributes:
label: 'Steps To Reproduce:'
description: Steps to reproduce the behavior.
placeholder: |
1. How did the configuration look like
2. What type of action was made
validations:
required: true
- type: checkboxes
attributes:
label: Are there any labels you wish to add?
description: Please search labels and identify those related to your bug.
options:
- label: I have added the relevant labels to the bug report.
required: true
- type: textarea
id: logs
attributes:
label: 'Relevant log output:'
description: >-
Please copy and paste any relevant log output. This will be
automatically formatted into code, so no need for backticks.
render: shell
- type: textarea
id: additional-context
attributes:
label: 'Additional context:'
description: Add any other context about the problem here.
================================================
FILE: .github/ISSUE_TEMPLATE/config.yml
================================================
blank_issues_enabled: false
contact_links:
- name: Ynput Community Discussions
url: https://community.ynput.io
about: Please ask and answer questions here.
- name: Ynput Discord Server
url: https://discord.gg/ynput
about: For community quick chats.
================================================
FILE: .github/ISSUE_TEMPLATE/enhancement_request.yml
================================================
name: Enhancement Request
description: Create a report to help us enhance a particular feature
title: "Enhancement: "
labels:
- "type: enhancement"
body:
- type: markdown
attributes:
value: |
Thanks for taking the time to fill out this enhancement request report!
- type: checkboxes
attributes:
label: Is there an existing issue for this?
description: Please search to see if an issue already exists for the bug you encountered.
options:
- label: I have searched the existing issues.
required: true
- type: textarea
id: related-feature
attributes:
label: Please describe the feature you have in mind and explain what the current shortcomings are?
description: A clear and concise description of what the problem is.
validations:
required: true
- type: textarea
id: enhancement-proposal
attributes:
label: How would you imagine the implementation of the feature?
description: A clear and concise description of what you want to happen.
validations:
required: true
- type: checkboxes
attributes:
label: Are there any labels you wish to add?
description: Please search labels and identify those related to your enhancement.
options:
- label: I have added the relevant labels to the enhancement request.
required: true
- type: textarea
id: alternatives
attributes:
label: "Describe alternatives you've considered:"
description: A clear and concise description of any alternative solutions or features you've considered.
validations:
required: false
- type: textarea
id: additional-context
attributes:
label: "Additional context:"
description: Add any other context or screenshots about the enhancement request here.
validations:
required: false
================================================
FILE: .github/pr-branch-labeler.yml
================================================
# Apply label "feature" if head matches "feature/*"
'type: feature':
head: "feature/*"
# Apply label "feature" if head matches "feature/*"
'type: enhancement':
head: "enhancement/*"
# Apply label "bugfix" if head matches one of "bugfix/*" or "hotfix/*"
'type: bug':
head: ["bugfix/*", "hotfix/*"]
# Apply label "release" if base matches "release/*"
'Bump Minor':
base: "release/next-minor"
================================================
FILE: .github/pr-glob-labeler.yml
================================================
# Add type: unittest label if any changes in tests folders
'type: unittest':
- '*/*tests*/**/*'
# any changes in documentation structure
'type: documentation':
- '*/**/*website*/**/*'
- '*/**/*docs*/**/*'
# hosts triage
'host: Nuke':
- '*/**/*nuke*'
- '*/**/*nuke*/**/*'
'host: Photoshop':
- '*/**/*photoshop*'
- '*/**/*photoshop*/**/*'
'host: Harmony':
- '*/**/*harmony*'
- '*/**/*harmony*/**/*'
'host: UE':
- '*/**/*unreal*'
- '*/**/*unreal*/**/*'
'host: Houdini':
- '*/**/*houdini*'
- '*/**/*houdini*/**/*'
'host: Maya':
- '*/**/*maya*'
- '*/**/*maya*/**/*'
'host: Resolve':
- '*/**/*resolve*'
- '*/**/*resolve*/**/*'
'host: Blender':
- '*/**/*blender*'
- '*/**/*blender*/**/*'
'host: Hiero':
- '*/**/*hiero*'
- '*/**/*hiero*/**/*'
'host: Fusion':
- '*/**/*fusion*'
- '*/**/*fusion*/**/*'
'host: Flame':
- '*/**/*flame*'
- '*/**/*flame*/**/*'
'host: TrayPublisher':
- '*/**/*traypublisher*'
- '*/**/*traypublisher*/**/*'
'host: 3dsmax':
- '*/**/*max*'
- '*/**/*max*/**/*'
'host: TV Paint':
- '*/**/*tvpaint*'
- '*/**/*tvpaint*/**/*'
'host: CelAction':
- '*/**/*celaction*'
- '*/**/*celaction*/**/*'
'host: After Effects':
- '*/**/*aftereffects*'
- '*/**/*aftereffects*/**/*'
'host: Substance Painter':
- '*/**/*substancepainter*'
- '*/**/*substancepainter*/**/*'
# modules triage
'module: Deadline':
- '*/**/*deadline*'
- '*/**/*deadline*/**/*'
'module: RoyalRender':
- '*/**/*royalrender*'
- '*/**/*royalrender*/**/*'
'module: Sitesync':
- '*/**/*sync_server*'
- '*/**/*sync_server*/**/*'
'module: Ftrack':
- '*/**/*ftrack*'
- '*/**/*ftrack*/**/*'
'module: Shotgrid':
- '*/**/*shotgrid*'
- '*/**/*shotgrid*/**/*'
'module: Kitsu':
- '*/**/*kitsu*'
- '*/**/*kitsu*/**/*'
================================================
FILE: .github/pull_request_template.md
================================================
## Changelog Description
Paragraphs contain detailed information on the changes made to the product or service, providing an in-depth description of the updates and enhancements. They can be used to explain the reasoning behind the changes, or to highlight the importance of the new features. Paragraphs can often include links to further information or support documentation.
## Additional info
Paragraphs of text giving context of additional technical information or code examples.
## Testing notes:
1. start with this step
2. follow this step
================================================
FILE: .github/workflows/documentation.yml
================================================
name: 📜 Documentation
on:
pull_request:
branches: [develop]
types: [review_requested, ready_for_review]
paths:
- 'website/**'
push:
branches: [main]
paths:
- 'website/**'
workflow_dispatch:
jobs:
check-build:
if: github.event_name != 'push'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v1
- uses: actions/setup-node@v1
with:
node-version: 14.x
cache: yarn
- name: Test Build
run: |
cd website
if [ -e yarn.lock ]; then
yarn install --frozen-lockfile
elif [ -e package-lock.json ]; then
npm ci
else
npm i
fi
npm run build
deploy-website:
if: github.event_name != 'pull_request'
runs-on: ubuntu-latest
steps:
- name: 🚚 Get latest code
uses: actions/checkout@v2
- uses: actions/setup-node@v1
with:
node-version: 14.x
cache: yarn
- name: 🔨 Build
run: |
cd website
if [ -e yarn.lock ]; then
yarn install --frozen-lockfile
elif [ -e package-lock.json ]; then
npm ci
else
npm i
fi
npm run build
- name: 📂 Sync files
uses: SamKirkland/FTP-Deploy-Action@4.0.0
with:
server: ftp.openpype.io
username: ${{ secrets.ftp_user }}
password: ${{ secrets.ftp_password }}
local-dir: ./website/build/
================================================
FILE: .github/workflows/milestone_assign.yml
================================================
name: 👉🏻 Milestone - assign to PRs
on:
pull_request_target:
types: [closed]
jobs:
run_if_release:
if: startsWith(github.base_ref, 'release/')
runs-on: ubuntu-latest
steps:
- name: 'Assign Milestone [next-minor]'
if: github.event.pull_request.milestone == null
uses: zoispag/action-assign-milestone@v1
with:
repo-token: "${{ secrets.YNPUT_BOT_TOKEN }}"
milestone: 'next-minor'
run_if_develop:
if: ${{ github.base_ref == 'develop' }}
runs-on: ubuntu-latest
steps:
- name: 'Assign Milestone [next-patch]'
if: github.event.pull_request.milestone == null
uses: zoispag/action-assign-milestone@v1
with:
repo-token: "${{ secrets.YNPUT_BOT_TOKEN }}"
milestone: 'next-patch'
================================================
FILE: .github/workflows/milestone_create.yml
================================================
name: ➕ Milestone - create default
on:
milestone:
types: [closed, edited]
jobs:
generate-next-patch:
runs-on: ubuntu-latest
steps:
- name: 'Get Milestones'
uses: "WyriHaximus/github-action-get-milestones@master"
id: milestones
env:
GITHUB_TOKEN: "${{ secrets.YNPUT_BOT_TOKEN }}"
- run: printf "name=number::%s" $(printenv MILESTONES | jq --arg MILESTONE $(printenv MILESTONE) '.[] | select(.title == $MILESTONE) | .number')
id: querymilestone
env:
MILESTONES: ${{ steps.milestones.outputs.milestones }}
MILESTONE: "next-patch"
- name: Read output
run: |
echo "${{ steps.querymilestone.outputs.number }}"
- name: 'Create `next-patch` milestone'
if: steps.querymilestone.outputs.number == ''
id: createmilestone
uses: "WyriHaximus/github-action-create-milestone@v1"
with:
title: 'next-patch'
env:
GITHUB_TOKEN: "${{ secrets.YNPUT_BOT_TOKEN }}"
generate-next-minor:
runs-on: ubuntu-latest
steps:
- name: 'Get Milestones'
uses: "WyriHaximus/github-action-get-milestones@master"
id: milestones
env:
GITHUB_TOKEN: "${{ secrets.YNPUT_BOT_TOKEN }}"
- run: printf "name=number::%s" $(printenv MILESTONES | jq --arg MILESTONE $(printenv MILESTONE) '.[] | select(.title == $MILESTONE) | .number')
id: querymilestone
env:
MILESTONES: ${{ steps.milestones.outputs.milestones }}
MILESTONE: "next-minor"
- name: Read output
run: |
echo "${{ steps.querymilestone.outputs.number }}"
- name: 'Create `next-minor` milestone'
if: steps.querymilestone.outputs.number == ''
id: createmilestone
uses: "WyriHaximus/github-action-create-milestone@v1"
with:
title: 'next-minor'
env:
GITHUB_TOKEN: "${{ secrets.YNPUT_BOT_TOKEN }}"
================================================
FILE: .github/workflows/miletone_release_trigger.yml
================================================
name: 🚩 Milestone Release [trigger]
on:
workflow_dispatch:
inputs:
milestone:
required: true
milestone:
types: closed
jobs:
milestone-title:
runs-on: ubuntu-latest
outputs:
milestone: ${{ steps.milestoneTitle.outputs.value }}
steps:
- name: Switch input milestone
uses: haya14busa/action-cond@v1
id: milestoneTitle
with:
cond: ${{ inputs.milestone == '' }}
if_true: ${{ github.event.milestone.title }}
if_false: ${{ inputs.milestone }}
- name: Print resulted milestone
run: |
echo "${{ steps.milestoneTitle.outputs.value }}"
call-ci-tools-milestone-release:
needs: milestone-title
uses: ynput/ci-tools/.github/workflows/milestone_release_ref.yml@main
with:
milestone: ${{ needs.milestone-title.outputs.milestone }}
repo-owner: ${{ github.event.repository.owner.login }}
repo-name: ${{ github.event.repository.name }}
version-py-path: "./openpype/version.py"
pyproject-path: "./pyproject.toml"
secrets:
token: ${{ secrets.YNPUT_BOT_TOKEN }}
user_email: ${{ secrets.CI_EMAIL }}
user_name: ${{ secrets.CI_USER }}
cu_api_key: ${{ secrets.CLICKUP_API_KEY }}
cu_team_id: ${{ secrets.CLICKUP_TEAM_ID }}
cu_field_id: ${{ secrets.CLICKUP_RELEASE_FIELD_ID }}
================================================
FILE: .github/workflows/nightly_merge.yml
================================================
name: 🔀 Dev -> Main
on:
schedule:
- cron: '21 3 * * 3,6'
workflow_dispatch:
jobs:
develop-to-main:
runs-on: ubuntu-latest
steps:
- name: 🚛 Checkout Code
uses: actions/checkout@v2
- name: 🔨 Merge develop to main
uses: everlytic/branch-merge@1.1.0
with:
github_token: ${{ secrets.YNPUT_BOT_TOKEN }}
source_ref: 'develop'
target_branch: 'main'
commit_message_template: '[Automated] Merged {source_ref} into {target_branch}'
- name: Invoke pre-release workflow
uses: benc-uk/workflow-dispatch@v1
with:
workflow: prerelease.yml
token: ${{ secrets.YNPUT_BOT_TOKEN }}
================================================
FILE: .github/workflows/pr_labels.yml
================================================
name: 🔖 PR labels
on:
pull_request_target:
types: [opened, assigned]
jobs:
size-label:
name: pr_size_label
runs-on: ubuntu-latest
if: github.event.action == 'assigned' || github.event.action == 'opened'
steps:
- name: Add size label
uses: "pascalgn/size-label-action@v0.4.3"
env:
GITHUB_TOKEN: "${{ secrets.YNPUT_BOT_TOKEN }}"
IGNORED: ".gitignore\n*.md\n*.json"
with:
sizes: >
{
"0": "XS",
"100": "S",
"500": "M",
"1000": "L",
"1500": "XL",
"2500": "XXL"
}
label_prs_branch:
name: pr_branch_label
runs-on: ubuntu-latest
if: github.event.action == 'assigned' || github.event.action == 'opened'
steps:
- name: Label PRs - Branch name detection
uses: ffittschen/pr-branch-labeler@v1
with:
repo-token: ${{ secrets.YNPUT_BOT_TOKEN }}
label_prs_globe:
name: pr_globe_label
runs-on: ubuntu-latest
if: github.event.action == 'assigned' || github.event.action == 'opened'
steps:
- name: Label PRs - Globe detection
uses: actions/labeler@v4.0.3
with:
repo-token: ${{ secrets.YNPUT_BOT_TOKEN }}
configuration-path: ".github/pr-glob-labeler.yml"
sync-labels: false
================================================
FILE: .github/workflows/prerelease.yml
================================================
name: ⏳ Nightly Prerelease
on:
workflow_dispatch:
jobs:
create_nightly:
runs-on: ubuntu-latest
steps:
- name: 🚛 Checkout Code
uses: actions/checkout@v2
with:
fetch-depth: 0
- name: Set up Python
uses: actions/setup-python@v2
with:
python-version: 3.9
- name: Install Python requirements
run: pip install gitpython semver PyGithub
- name: 🔎 Determine next version type
id: version_type
run: |
TYPE=$(python ./tools/ci_tools.py --bump --github_token ${{ secrets.YNPUT_BOT_TOKEN }})
echo "type=${TYPE}" >> $GITHUB_OUTPUT
- name: 💉 Inject new version into files
id: version
if: steps.version_type.outputs.type != 'skip'
run: |
NEW_VERSION_TAG=$(python ./tools/ci_tools.py --nightly --github_token ${{ secrets.YNPUT_BOT_TOKEN }})
echo "next_tag=${NEW_VERSION_TAG}" >> $GITHUB_OUTPUT
- name: 💾 Commit and Tag
id: git_commit
if: steps.version_type.outputs.type != 'skip'
run: |
git config user.email ${{ secrets.CI_EMAIL }}
git config user.name ${{ secrets.CI_USER }}
git checkout main
git pull
git add .
git commit -m "[Automated] Bump version"
tag_name="CI/${{ steps.version.outputs.next_tag }}"
echo $tag_name
git tag -a $tag_name -m "nightly build"
- name: Push to protected main branch
uses: CasperWA/push-protected@v2.10.0
with:
token: ${{ secrets.YNPUT_BOT_TOKEN }}
branch: main
tags: true
unprotect_reviews: true
- name: 🔨 Merge main back to develop
uses: everlytic/branch-merge@1.1.0
if: steps.version_type.outputs.type != 'skip'
with:
github_token: ${{ secrets.YNPUT_BOT_TOKEN }}
source_ref: 'main'
target_branch: 'develop'
commit_message_template: '[Automated] Merged {source_ref} into {target_branch}'
- name: Invoke Update bug report workflow
uses: benc-uk/workflow-dispatch@v1
with:
workflow: update_bug_report.yml
token: ${{ secrets.YNPUT_BOT_TOKEN }}
================================================
FILE: .github/workflows/project_task_statuses.yml
================================================
name: 📊 Project task statuses
on:
pull_request_review:
types: [submitted]
issue_comment:
types: [created]
pull_request_review_comment:
types: [created]
jobs:
pr_review_started:
name: pr_review_started
runs-on: ubuntu-latest
# -----------------------------
# conditions are:
# - PR issue comment which is not form Ynbot
# - PR review comment which is not Hound (or any other bot)
# - PR review submitted which is not from Hound (or any other bot) and is not 'Changes requested'
# - make sure it only runs if not forked repo
# -----------------------------
if: |
(github.event_name == 'issue_comment' && github.event.pull_request.head.repo.owner.login == 'ynput' && github.event.comment.user.id != 82967070) ||
(github.event_name == 'pull_request_review_comment' && github.event.pull_request.head.repo.owner.login == 'ynput' && github.event.comment.user.type != 'Bot') ||
(github.event_name == 'pull_request_review' &&
github.event.pull_request.head.repo.owner.login == 'ynput' &&
github.event.review.state != 'changes_requested' &&
github.event.review.state != 'approved' &&
github.event.review.user.type != 'Bot')
steps:
- name: Move PR to 'Review In Progress'
uses: leonsteinhaeuser/project-beta-automations@v2.1.0
with:
gh_token: ${{ secrets.YNPUT_BOT_TOKEN }}
organization: ynput
project_id: 11
resource_node_id: ${{ github.event.pull_request.node_id || github.event.issue.node_id }}
status_value: Review In Progress
pr_review_requested:
# -----------------------------
# Resets Clickup Task status to 'In Progress' after 'Changes Requested' were submitted to PR
# It only runs if custom clickup task id was found in ref branch of PR
# -----------------------------
name: pr_review_requested
runs-on: ubuntu-latest
if: github.event_name == 'pull_request_review' && github.event.pull_request.head.repo.owner.login == 'ynput' && github.event.review.state == 'changes_requested'
steps:
- name: Set branch env
run: echo "BRANCH_NAME=${{ github.event.pull_request.head.ref}}" >> $GITHUB_ENV
- name: Get ClickUp ID from ref head name
id: get_cuID
run: |
echo ${{ env.BRANCH_NAME }}
echo "cuID=$(echo $BRANCH_NAME | sed 's/.*\/\(OP\-[0-9]\{4\}\).*/\1/')" >> $GITHUB_OUTPUT
- name: Print ClickUp ID
run: echo ${{ steps.get_cuID.outputs.cuID }}
- name: Move found Clickup task to 'Review in Progress'
if: steps.get_cuID.outputs.cuID
run: |
curl -i -X PUT \
'https://api.clickup.com/api/v2/task/${{ steps.get_cuID.outputs.cuID }}?custom_task_ids=true&team_id=${{secrets.CLICKUP_TEAM_ID}}' \
-H 'Authorization: ${{secrets.CLICKUP_API_KEY}}' \
-H 'Content-Type: application/json' \
-d '{
"status": "in progress"
}'
================================================
FILE: .github/workflows/test_build.yml
================================================
# This workflow will upload a Python Package using Twine when a release is created
# For more information see: https://help.github.com/en/actions/language-and-framework-guides/using-python-with-github-actions#publishing-to-package-registries
name: 🏗️ Test Build
on:
pull_request:
branches: [develop]
types: [review_requested, ready_for_review]
paths-ignore:
- 'docs/**'
- 'website/**'
- 'vendor/**'
jobs:
Windows-latest:
runs-on: windows-latest
strategy:
matrix:
python-version: [3.9]
steps:
- name: 🚛 Checkout Code
uses: actions/checkout@v2
- name: Set up Python
uses: actions/setup-python@v2
with:
python-version: ${{ matrix.python-version }}
- name: 🧵 Install Requirements
shell: pwsh
run: |
./tools/create_env.ps1
- name: 🔨 Build
shell: pwsh
run: |
$env:SKIP_THIRD_PARTY_VALIDATION="1"
./tools/build.ps1
Ubuntu-latest:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: [3.9]
steps:
- name: 🚛 Checkout Code
uses: actions/checkout@v2
- name: Set up Python
uses: actions/setup-python@v2
with:
python-version: ${{ matrix.python-version }}
- name: 🧵 Install Requirements
run: |
./tools/create_env.sh
- name: 🔨 Build
run: |
export SKIP_THIRD_PARTY_VALIDATION="1"
./tools/build.sh
================================================
FILE: .github/workflows/update_bug_report.yml
================================================
name: 🐞 Update Bug Report
on:
workflow_dispatch:
release:
# https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#release
types: [published]
jobs:
update-bug-report:
runs-on: ubuntu-latest
name: Update bug report
steps:
- uses: actions/checkout@v3
with:
ref: ${{ github.event.release.target_commitish }}
- name: Update version
uses: ynput/gha-populate-form-version@main
with:
github_token: ${{ secrets.YNPUT_BOT_TOKEN }}
registry: github
dropdown: _version
limit_to: 100
form: .github/ISSUE_TEMPLATE/bug_report.yml
commit_message: 'chore(): update bug report / version'
dry_run: no-push
- name: Push to protected develop branch
uses: CasperWA/push-protected@v2.10.0
with:
token: ${{ secrets.YNPUT_BOT_TOKEN }}
branch: develop
unprotect_reviews: true
================================================
FILE: .gitignore
================================================
# Created by .ignore support plugin (hsz.mobi)
### Python template
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# Mac Stuff
###########
# General
.DS_Store
.AppleDouble
.LSOverride
# Icon must end with two \r
Icon
# Thumbnails
._*
# rope project dir
.ropeproject
# Files that might appear in the root of a volume
.DocumentRevisions-V100
.fseventsd
.Spotlight-V100
.TemporaryItems
.Trashes
.VolumeIcon.icns
.com.apple.timemachine.donotpresent
# Directories potentially created on remote AFP share
.AppleDB
.AppleDesktop
Network Trash Folder
Temporary Items
.apdisk
# CX_Freeze
###########
/build
/dist/
/server_addon/packages/*
/vendor/bin/*
/vendor/python/*
/.venv
/venv/
# Documentation
###############
/docs/build
# Editor backup files #
#######################
*~
# Unit test / coverage reports
##############################
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
/coverage
.cache
nosetests.xml
coverage.xml
*.cover
.hypothesis/
.pytest_cache/
# Node JS packages
##################
node_modules
package-lock.json
package.json
yarn.lock
openpype/premiere/ppro/js/debug.log
# IDEA
######
.idea/
# VScode files
.vscode/
.env
dump.sql
test_localsystem.txt
# website
##########
website/translated_docs
website/build/
website/node_modules
website/i18n/*
website/debug.log
website/.docusaurus
# Poetry
########
.poetry/
.python-version
.editorconfig
.pre-commit-config.yaml
mypy.ini
tools/run_eventserver.*
# Developer tools
tools/dev_*
.github_changelog_generator
# Addons
########
/openpype/addons/*
!/openpype/addons/README.md
================================================
FILE: .gitmodules
================================================
[submodule "tools/modules/powershell/BurntToast"]
path = tools/modules/powershell/BurntToast
url = https://github.com/Windos/BurntToast.git
[submodule "tools/modules/powershell/PSWriteColor"]
path = tools/modules/powershell/PSWriteColor
url = https://github.com/EvotecIT/PSWriteColor.git
[submodule "openpype/hosts/unreal/integration"]
path = openpype/hosts/unreal/integration
url = https://github.com/ynput/ayon-unreal-plugin.git
================================================
FILE: .hound.yml
================================================
flake8:
enabled: true
config_file: setup.cfg
================================================
FILE: .prettierrc
================================================
{
"tabWidth": 4
}
================================================
FILE: ARCHITECTURE.md
================================================
# Architecture
OpenPype is a monolithic Python project that bundles several parts, this document will try to give a birds eye overview of the project and, to a certain degree, each of the sub-projects.
The current file structure looks like this:
```
.
├── common - Code in this folder is backend portion of Addon distribution logic for v4 server.
├── docs - Documentation of the source code.
├── igniter - The OpenPype bootstrapper, deals with running version resolution and setting up the connection to the mongodb.
├── openpype - The actual OpenPype core package.
├── schema - Collection of JSON files describing schematics of objects. This follows Avalon's convention.
├── tests - Integration and unit tests.
├── tools - Conveninece scripts to perform common actions (in both bash and ps1).
├── vendor - When using the igniter, it deploys third party tools in here, such as ffmpeg.
└── website - Source files for https://openpype.io/ which is Docusaursus (https://docusaurus.io/).
```
The core functionality of the pipeline can be found in `igniter` and `openpype`, which in turn rely on the `schema` files, whenever you build (or download a pre-built) version of OpenPype, these two are bundled in there, and `Igniter` is the entry point.
## Igniter
It's the setup and update tool for OpenPype, unless you want to package `openpype` separately and deal with all the config manually, this will most likely be your entry point.
```
igniter/
├── bootstrap_repos.py - Module that will find or install OpenPype versions in the system.
├── __init__.py - Igniter entry point.
├── install_dialog.py- Show dialog for choosing central pype repository.
├── install_thread.py - Threading helpers for the install process.
├── __main__.py - Like `__init__.py` ?
├── message_dialog.py - Qt Dialog with a message and "Ok" button.
├── nice_progress_bar.py - Fancy Qt progress bar.
├── splash.txt - ASCII art for the terminal installer.
├── stylesheet.css - Installer Qt styles.
├── terminal_splash.py - Terminal installer animation, relies in `splash.txt`.
├── tools.py - Collection of methods that don't fit in other modules.
├── update_thread.py - Threading helper to update existing OpenPype installs.
├── update_window.py - Qt UI to update OpenPype installs.
├── user_settings.py - Interface for the OpenPype user settings.
└── version.py - Igniter's version number.
```
## OpenPype
This is the main package of the OpenPype logic, it could be loosely described as a combination of [Avalon](https://getavalon.github.io), [Pyblish](https://pyblish.com/) and glue around those with custom OpenPype only elements, things are in progress of being moved around to better prepare for V4, which will be released under a new name AYON.
```
openpype/
├── client - Interface for the MongoDB.
├── hooks - Hooks to be executed on certain OpenPype Applications defined in `openpype.lib.applications`.
├── host - Base class for the different hosts.
├── hosts - Integration with the different DCCs (hosts) using the `host` base class.
├── lib - Libraries that stitch together the package, some have been moved into other parts.
├── modules - OpenPype modules should contain separated logic of specific kind of implementation, such as Ftrack connection and its python API.
├── pipeline - Core of the OpenPype pipeline, handles creation of data, publishing, etc.
├── plugins - Global/core plugins for loader and publisher tool.
├── resources - Icons, fonts, etc.
├── scripts - Loose scipts that get run by tools/publishers.
├── settings - OpenPype settings interface.
├── style - Qt styling.
├── tests - Unit tests.
├── tools - Core tools, check out https://openpype.io/docs/artist_tools.
├── vendor - Vendoring of needed required Python packes.
├── widgets - Common re-usable Qt Widgets.
├── action.py - LEGACY: Lives now in `openpype.pipeline.publish.action` Pyblish actions.
├── cli.py - Command line interface, leverages `click`.
├── __init__.py - Sets two constants.
├── __main__.py - Entry point, calls the `cli.py`
├── plugin.py - Pyblish plugins.
├── pype_commands.py - Implementation of OpenPype commands.
└── version.py - Current version number.
```
================================================
FILE: CHANGELOG.md
================================================
# Changelog
## [3.18.11](https://github.com/ynput/OpenPype/tree/3.18.11)
[Full Changelog](https://github.com/ynput/OpenPype/compare/3.18.10...3.18.11)
### **🚀 Enhancements**
Sourced from webpack's releases. Full Changelog: https://github.com/webpack/webpack/compare/v5.75.0...v5.76.0 ... (truncated) This version was pushed to npm by evilebottnawi, a new releaser for webpack since your current version. This version was pushed to npm by marsup, a new releaser for Sourced from ua-parser-js's changelog. Version 0.8 was created by accident. This version is now deprecated and no longer maintained, please update to version 0.7 / 1.0.Deadline: Houdini submission settings in OP #6269
Make houdini submissions respect pools groups.This is done by:
- Make collect pools works with some Houdini families/product types.
- Make Ayon Houdini submitters get group names from Houdini deadline settings.
___
Maya: Ensure unique class name compared to `extract_yeti_cache.py` #6251
Fix duplicate `ExtractYetiCache` plug-in name.
___
Maya: Correct Alembic export defaults AY-5273 #6268
Missing `writeUVs` on the Alembic extraction.
___
Arnold Scene Source Raw - OP-8014 #6182
This PR is to try and re-instate some flexibility to the `Arnold Scene Source` family, which got restricted by https://github.com/ynput/OpenPype/pull/4449The proxy workflow introduced was actually broken due to https://github.com/ynput/OpenPype/pull/4460.We can now have any nodes directly in the instance set, which should be backwards compatible of the `Arnold Scene Source` before the overhaul in https://github.com/ynput/OpenPype/pull/4449.The `content` and `proxy` sets works as well, but not at the same time as the raw nodes directly in the instance set. There is a validator in place to prevent using a single instance for both workflows.Now the question is whether we should have this as a single family or split somehow?The workflow of having nodes directly in the instance set, compared to `content` and `proxy` set, can be documented, so I see this as most a matter of terminology.`Arnold Scene Source` makes sense to have as a family, but only if its a the raw output with little to no validation, similar to `Maya Scene`. But then I'm not sure what to call the other family that has more of a workflow in place, which is similar to `Model` and `Pointcache`.
___
Nuke: Push to project - AY-742 #6245
This introduces the "Push to Project" menu item in Nuke.This enables users to push the current workfile to a different project and copy all files from Read nodes to a `resources` folder next to the workfile. Containers will be baked to normal Read nodes.Also gizmos will be baked to groups.
___
Max: Implementation of Validate Render Passes #6138
This PR is to enhance the current validator of checking the render output before deadline publish. It does the following:
- The validator `Render Output for Deadline` would be renamed as `Validate Render Passes`
- The validator would not only check on the invalid render output folder but the invalid filename of render passes.
___
Max : Optional validator to check invalid context data #6198
Add optional validator check on invalid context data for asset and task in 3dsMax
___
Maya: Account for no Alembic overrides. #6267
Fix for if no overrides are present in `project_settings/maya/publish/ExtractAlembic/overrides`
___
Integration: 3DEqualizer integration #5868
This PR is adding basic integration for 3DEqualizer4 from Science-D-Vision. Integration includes:
- Workfiles
- Loading plates (cameras)
- Publishing scripts to Maya and Nuke
- Publishing of lens data
___
Maya: abc options for Pointcache/Animation family - OP-5920 #5173
Add all options for alembic extraction on `pointcache` and `animation` families.
___
RoyalRender: environment injection on the server #6160
Previously env vars were injected directly on the client during submission. That could have issues when environment on client machines is different than on workers.This PR tries similar approach as on DL when before job is rendered it queries Ayon to get environment variables for context.These variables are used to create `.rrEnv` file and attach it to the job. That should provide rendering environment controlled by Ayon.
___
Hiero: colorspace settings aligned with nuke - AY-978 #6249
In order to share the same colorspaces in the workfile in Hiero and Nuke, we need to bring back the workfile settings for colorspaces in Nuke.In Hiero we also need code to edit the project settings in memory and apply the colorspaces when launching Hiero so any new project gets the correct colorspaces. Due to Foundry not providing Python API methods for setting the project colorspaces, we need to go through the UI widgets to set them, when dealing with in-memory projects.Also small bugfix when saving the workfile without any sequences.
___
Maya: Make sure validators being shown in the Publisher UI when they set to be optional in AYON setting #6257
This PR is to make sure validators being shown correctly in the Publisher UI when they are being set to be optional in AYON setting.Ported from https://github.com/ynput/ayon-core/pull/201
___
Maya: Fix Redshift cryptomatte multipartEXR #6240
When using Redshift and rendering multipart EXRs, the instances for cryptomatte AOVs are getting falsely marked as multipart EXR even though they are being forced to be separate files by Redshift.Since we cannot query the AOVs multipart individually, we'll need a hardcoded rule.Ideally I guess AOVs should be separate instances in the publishing process but that is too big of a scope atm.
___
Substance Painter: Allow users to set texture resolutions when loading mesh to create project #6262
This PR is to add the support of template settings in the mesh loaders for Substance project creation. User can customize and add template settings in AYON settings and apply it through the option mode(the button with memo icon).
___
Deadline: Submit Publish job error #6263
Use get env to get the value of `AVALON_DB``AVALON_DB` environment variable is not initialized when using OpenPype in Ayon mode. which raise an error when using `os.environ["AVALON_DB"]`This PR changes it to `os.getenv("AVALON_DB")`
___
Fix: Removed double conversion of limit_groups #6265
`limit_groups` settings got transformed twice. Kept nicer looking conversion.
___
Max: Implementation of Camera Attributes Validator #6110
Implement Validate Camera Attributes in camera family in Max host
___
Max: Add missing workfile creator #6203
Add the missing workfile creator in 3dsMax.
___
Deadline: Expose families transfer setting - OP-8268 #6217
This PR exposes the `families_transfer` attribute on the `ProcessSubmittedJobOnFarm` plugin.The use case is to remove `ftrack` from the list if a studio does not want all render passes from Maya to become asset versions in Ftrack.
___
Deadline: Add AVALON_DB to Deadline submissions - OP-8270 #6218
Because testing uses a different database name https://github.com/ynput/OpenPype/blob/develop/tests/lib/testing_classes.py#L46 we need to add `AVALON_DB` to the environment for Deadline submissions.
___
Houdini: fix default render product name in Vray #6083
This is fixing key name for default render products in VRay. Original name `RGB Color` caused issues during job submission.
___
Resolve Clip Load - Slate support #6126
Loaded clip should ignore the slate, and be trimmed the same regardless of slate presence.closes: https://github.com/ynput/OpenPype/issues/6124#AY-1684
___
Use duration from streams as its more precise #6171
When dealing with 30 fps mov of 2 frames, the duration was reduce to 3 decimal places (0.067) which meant that the flag for ffmpeg `-ss` ended up with a time that was not precise enough for ffmpeg to pick a frame; `0.0335`. Should be `0.0333`.Using the duration from the streams is more precise; `0.066667`.
___
Core: Headless publish failing without GL lib #6205
Trying to run a headless publish in the farm I hit another blocker:
```
2024-02-07 20:42:45: 0: STDOUT: !!! AYON crashed:
2024-02-07 20:42:45: 0: STDOUT: Traceback (most recent call last):
2024-02-07 20:42:45: 0: STDOUT: File "start.py", line 740, in main_cli
2024-02-07 20:42:45: 0: STDOUT: ))
2024-02-07 20:42:45: 0: STDOUT: File "/usr/ayon-launcher/1.0.0+ax/dependencies/click/core.py", line 1157, in __call__
2024-02-07 20:42:45: 0: STDOUT: return self.main(*args, **kwargs)
2024-02-07 20:42:45: 0: STDOUT: File "/usr/ayon-launcher/1.0.0+ax/dependencies/click/core.py", line 1078, in main
2024-02-07 20:42:45: 0: STDOUT: rv = self.invoke(ctx)
2024-02-07 20:42:45: 0: STDOUT: File "/usr/ayon-launcher/1.0.0+ax/dependencies/click/core.py", line 1688, in invoke
2024-02-07 20:42:45: 0: STDOUT: return _process_result(sub_ctx.command.invoke(sub_ctx))
2024-02-07 20:42:45: 0: STDOUT: File "/usr/ayon-launcher/1.0.0+ax/dependencies/click/core.py", line 1434, in invoke
2024-02-07 20:42:45: 0: STDOUT: return ctx.invoke(self.callback, **ctx.params)
2024-02-07 20:42:45: 0: STDOUT: File "/usr/ayon-launcher/1.0.0+ax/dependencies/click/core.py", line 783, in invoke
2024-02-07 20:42:45: 0: STDOUT: return __callback(*args, **kwargs)
2024-02-07 20:42:45: 0: STDOUT: File "/pipe/dev/farrizabalaga/OpenPype/openpype/cli.py", line 197, in publish
2024-02-07 20:42:45: 0: STDOUT: PypeCommands.publish(list(paths), targets, gui)
2024-02-07 20:42:45: 0: STDOUT: File "/pipe/dev/farrizabalaga/OpenPype/openpype/pype_commands.py", line 100, in publish
2024-02-07 20:42:45: 0: STDOUT: from openpype.tools.utils.host_tools import show_publish
2024-02-07 20:42:45: 0: STDOUT: File "/pipe/dev/farrizabalaga/OpenPype/openpype/tools/utils/__init__.py", line 1, in Nuke: LoadClip colorspace override - OP-6591 #6215
Setting the colorspace from the representation data was not supported.
___
Hiero: Add OP settings and convert in plugin - OP-8338 #6232
Missing settings for https://github.com/ynput/OpenPype/pull/6143.
___
Unreal: Fix Render Instance Collector to use folderPath #6233
Fix Render Instance Collector to use folderPath instead of just the asset name.
___
Bugfix - Fix "Action Failed" window not showing #6236
This PR targets to fix issue #6234.
___
Nuke: render use existing frames with slate offsets the published render - AY-1433 #6239
Due to `frameStart` data member on representation for existing frames, the frame indexes would be re-numbered when integrating due to this; https://github.com/ynput/OpenPype/blob/develop/openpype/plugins/publish/integrate.py#L712-L726Removing `frameStart` had no effect on publishing workflows, local or farm.Also fixed an issues with slate collection which could misbehave if the instance node had "slate" in the name.Resolves #5883
___
AYON Workfiles tool: Copy and open of published workfile works #6241
Fix copy and open published workfiles.
___
Chore: OCIO and python2 compatibility fixes #6242
Nuke 12 is now fully supported with our OCIO wrapping functionalities.
___
Tests: Fix failing maya automatic test #6235
Improvement on https://github.com/ynput/OpenPype/pull/6231
___
Chore: Wrapper for click proposal #5928
This is a proposal how to resolve issues with `click` python module. Issue https://github.com/ynput/OpenPype/issues/5921 reported that in Houdini 20+ is our click clashing with click in houdini, where is expected higher version. We can't update our version to support older pythons (NOTE older Python 3).
___
Maya: Add repair action to hidden joints validator #6214
Joints Hidden is missing repair action, this adds it back
___
Blender: output node and EXR #6086
Output node now works correctly for Multilayer EXR and keeps existing links. The output now is handled entirely by the compositor node tree.
___
AYON Switch tool: Keep version after switch #6104
Keep version if only representation did change. The AYON variant of https://github.com/ynput/OpenPype/pull/4629
___
Loader AYON: Reset loader window on open #6170
Make sure loader tool is reset on each show.
___
Publisher: Show message with error on action failure #6179
This PR adds support for the publisher to show error message from running actions.Errors from actions will otherwise be hidden from user in various console outputs.Also include card for when action is finished.
___
AYON Applications: Remove djvview group from default applications #6188
The djv does not have group defined in models so the values are not used anywhere.
___
General: added fallback for broken ffprobe return #6189
Customer provided .exr returned width and height equal to 0 which caused error in `extract_thumbnail`. This tries to use oiiotool to get metadata about file, in our case it read it correctly.
___
Photoshop: High scaling in UIs #6190
Use `get_openpype_qt_app` to create `QApplication` in Photoshop.
___
Ftrack: Status update settings are not case insensitive. #6195
Make values for project_settings/ftrack/events/status_update case insensitive.
___
Thumbnail product filtering #6197
This PR introduces subset filtering for thumbnail extraction. This is to skip passes like zdepth which is not needed and can cause issues with extraction. Also speeds up publishing.
___
TimersManager: Idle dialog always on top #6201
Make stop timer dialog always on tophttps://app.clickup.com/t/6658547/OP-8033
___
AfterEffects: added toggle for applying values from DB during creation #6204
Previously values (resolution, duration) from Asset (eg. DB) were applied explicitly when instance of `render` product type was created. This PR adds toggle to Settings to disable this. (This allows artist to publish non standard length of composition, disabling of `Validate Scene Settings` is still required.)
___
Unreal: Update plugin commit #6208
Updated unreal plugin to latest main.
___
Traypublisher: editorial avoid audio tracks processing #6038
Avoiding audio tracks from EDL editorial publishing.
___
Resolve Inventory offsets clips when swapping versions #6128
Swapped version retain the offset and IDT of the timelime clip.closes: https://github.com/ynput/OpenPype/issues/6125
___
Publisher window as dialog #6176
Changing back Publisher window to QDialog.
___
Nuke: Validate write node fix error report - OP-8088 #6183
Report error was not printing the expected values from settings, but instead the values on the write node, leading to confusing messages like:
```
Traceback (most recent call last):
File "C:\Users\tokejepsen\AppData\Local\Ynput\AYON\dependency_packages\ayon_2310271602_windows.zip\dependencies\pyblish\plugin.py", line 527, in __explicit_process
runner(*args)
File "C:\Users\tokejepsen\OpenPype\openpype\hosts\nuke\plugins\publish\validate_write_nodes.py", line 135, in process
self._make_error(check)
File "C:\Users\tokejepsen\OpenPype\openpype\hosts\nuke\plugins\publish\validate_write_nodes.py", line 149, in _make_error
raise PublishXmlValidationError(
openpype.pipeline.publish.publish_plugins.PublishXmlValidationError: Write node's knobs values are not correct!
Knob 'channels' > Correct: `rgb` > Wrong: `rgb`
```
This PR changes the error report to:
```
Traceback (most recent call last):
File "C:\Users\tokejepsen\AppData\Local\Ynput\AYON\dependency_packages\ayon_2310271602_windows.zip\dependencies\pyblish\plugin.py", line 527, in __explicit_process
runner(*args)
File "C:\Users\tokejepsen\OpenPype\openpype\hosts\nuke\plugins\publish\validate_write_nodes.py", line 135, in process
self._make_error(check)
File "C:\Users\tokejepsen\OpenPype\openpype\hosts\nuke\plugins\publish\validate_write_nodes.py", line 149, in _make_error
raise PublishXmlValidationError(
openpype.pipeline.publish.publish_plugins.PublishXmlValidationError: Write node's knobs values are not correct!
Knob 'channels' > Expected: `['rg']` > Current: `rgb`
```
___
Nuke: Camera product type loaded is not updating - OP-7973 #6184
When updating the camera this error would appear:
```
(...)openpype/hosts/nuke/plugins/load/load_camera_abc.py", line 142, in update
camera_node = nuke.toNode(object_name)
TypeError: toNode() argument 1 must be str, not Node
```
___
AYON settings: Use bundle name as variant in dev mode #6187
Make sure the bundle name is used in dev mode for settings variant.
___
Fusion: fix unwanted change to field name in Settings #6193
It should be `image_format` but in previous refactoring PR it fell back to original `output_formats` which caused enum not to show up and propagate into plugin.
___
Bugfix: AYON menu disappeared when the workspace has been changed in 3dsMax #6200
AYON plugins are not correctly registered when switching to different workspaces.
___
TrayPublisher: adding settings category to base creator classes #6202
Settings are resolving correctly as they suppose to.
___
Nuke: expose knobs backward compatibility fix - OP-8164 #6211
Fix backwards compatibility for settings `project_settings/nuke/create/CreateWriteRender/exposed_knobs`.
___
AE: fix local render doesn't push thumbnail to Ftrack #6212
Without thumbnail review is not clickable from main Versions list
___
Nuke: openpype expose knobs validator - OP-8166 #6213
Fix exposed knobs validator for backwards compatibility with missing settings.
___
Ftrack: Post-launch hook fix value lowering #6221
Fix lowerin of values in status mapping.
___
Maya: Remove `shelf` class and shelf build on maya `userSetup.py` #5837
Remove shelf builder logic. It appeared to be unused and had bugs.
___
Max: updated implementation of save_scene + small QOL improvements to host #6186
- Removed `has_unsaved_changes` from Max host as it looks to have been unused and unimplemented.
- Added and implemented `workfile_has_unsaved_changes` to Max host.
- Mirrored the Houdini host to implement the above into `save_scene` publish for Max.
- Added a line to `startup.ms` which opens the usual 'default' menu inside of Max (see screenshots).Current (Likely opens this menu due to one or more of the startup scripts used to insert OP menu):New:
___
Fusion: Use better resolution of Ayon apps on 4k display #6199
Changes size (makes it smaller) of Ayon apps (Workfiles, Loader) in Fusion on high definitions displays.
___
Update CONTRIBUTING.md #6210
Updating contributing guidelines to reflect the EOL state of repository
___
Deadline: Remove redundant instance_skeleton_data code - OP-8269 #6219
This PR https://github.com/ynput/OpenPype/pull/5186 re-introduced code about for the `instance_skeleton_data` but its actually not used since this variable gets overwritten later.
___
AYON: Use `SettingsField` from ayon server #6173
This is preparation for new version of pydantic which will require to customize the field class for AYON purposes as raw pydantic Field could not be used.
___
Nuke: Expose write knobs - OP-7592 #6137
This PR adds `exposed_knobs` to the creator plugins settings at `ayon+settings://nuke/create/CreateWriteRender/exposed_knobs`.When exposed knobs will be linked from the write node to the outside publish group, for users to adjust.
___
AYON: Remove kitsu addon #6172
Removed kitsu addon from server addons because already has own repository.
___
Fusion: provide better logging for validate saver crash due type error #6082
Handles reported issue for `NoneType` error thrown in conversion `int(tool["Comments"][frame])`. It is most likely happening when saver node has no input connections.There is a validator for that, but it might be not obvious, that this error is caused by missing input connections and it has been already reported by `"Validate Saver Has Input"`.
___
Workfile Template Builder: Use correct variable in create placeholder #6141
Use correct variable where failed instances are stored for validation.
___
ExtractOIIOTranscode: Missing product_names to subsets conversion #6159
The `Product Names` filtering should be fixed with this.
___
Blender: Fix missing animation data when updating blend assets #6165
Fix missing animation data when updating blend assets.
___
TrayPublisher: Pre-fill of version works in AYON #6180
Use `folderPath` instead of `asset` in AYON mode to calculate next available version.
___
Chore: remove Muster #6085
Muster isn't maintained for a long time and it wasn't working anyway. This is removing related code from the code base. If there is renewed interest in Muster, it needs to be re-implemented in modern AYON compatible way.
___
Maya: change label in the render settings to be more readable #6134
AYON replacement for #5713.
___
Chore: Add addons dir only if exists #6140
Do not add addons directory path for addons discovery if does not exists.
___
Hiero: Effect Categories - OP-7397 #6143
This PR introduces `Effect Categories` for the Hiero settings. This allows studios to split effect stacks into meaningful subsets.
___
Nuke: Render Workfile Attributes #6146
`Workfile Dependency` default value can now be controlled from project settings.`Use Published Workfile` makes using published workfiles for rendering optional.
___
Maya: Attributes are locked after publishing if they are locked in Camera Family #6073
This PR is to make sure unlock attributes only during the bake context, make sure attributes are relocked after to preserve the lock state of the original node being baked.
___
Missing nuke family Windows arguments #6131
Default Windows arguments for launching the Nuke family was missing.
___
AYON: Fix the bug on the limit group not being set correctly in Maya Deadline Setting #6139
This PR is to bug-fix the limit groups from maya deadline settings errored out when the user tries to edit the setting.
___
Chore: Transcoding extensions add missing '.tif' extension #6142
Image extensions in transcoding helper was missing `.tif` extension and had `.tiff` twice.
___
Blender: Use the new API for override context #6145
Blender 4.0 disabled the old API to override context. This API updates the code to use the new API.
___
BugFix: Include Model in FBX Loader in Houdini #6150
A quick bugfig where we can't load fbx exported from blender. The bug was reported here.
___
Blender: Restore actions to objects after update #6153
Restore the actions assigned to objects after updating assets from blend files.
___
Chore: Collect template data with hierarchy context #6154
Fixed queue loop where is used wrong variable to pop items from queue.
___
OP-6382 - Thumbnail Integration Problem #6156
This ticket alerted to 3 different cases of integration issues;
- [x] Using the Tray Publisher with the same image format (extension) for representation and review representation.
- [x] Clash on publish file path from output definitions in `ExtractOIIOTranscode`.
- [x] Clash on publish file from thumbnail in `ExtractThumbnail`There might be an issue with this fix, if a studio does not use the `{output}` token in their `render` anatomy template. But thinking if they have customized it, they will be responsible to maintain these edge cases.
___
Max: Bugfix saving camera scene errored out when creating render instance with multi-camera option turned off #6163
This PR is to make sure the integrator of saving camera scene turned off and the render submitted successfully when multi-camera options being turned off in 3dsmax
___
Chore: Fix duplicated project name on create project structure #6166
Small fix in project folders. It is not used same variable name to change values which breaks values on any next loop.
___
Maya: Remove duplicate plugin #6157
The two plugins below are doing the same work, so we can remove the one focused solely on lookdev.https://github.com/ynput/OpenPype/blob/develop/openpype/hosts/maya/plugins/publish/validate_look_members_unique.pyhttps://github.com/ynput/OpenPype/blob/develop/openpype/hosts/maya/plugins/publish/validate_node_ids_unique.py
___
Publish report viewer: Report items sorting #6092
Proposal of items sorting in Publish report viewer tool. Items are sorted by report creation time. Creation time is also added to publish report data when saved from publisher tool.
___
Maya: Extended error message #6161
Added more details to message
___
Fusion: Added settings for Fusion creators to legacy OP #6162
Added missing OP variant of setting for new Fusion creator.
___
multiple render camera supports for 3dsmax #5124
Supports for rendering with multiple cameras in 3dsmax
- [x] Add Batch Render Layers functions
- [x] Rewrite lib.rendersetting and lib.renderproduct
- [x] Add multi-camera options in creator.
- [x] Collector with batch render-layer when multi-camera enabled.
- [x] Add instance plugin for saving scene files with different cameras respectively by using subprocess
- [x] Refactor submit_max_deadline
- [x] Check with metadata.json in submit publish job
___
Fusion: new creator for image product type #6057
In many DCC `render` product type is expected to be sequence of files. This PR adds new explicit creator for `image` product type which is focused on single frame image. Workflows for both product types might be a bit different, this gives artists more granularity to choose better workflow.
___
Maya: Account and ignore free image planes. #5993
Free image planes do not have the `->` path separator, so we need to account for that.
___
Blender: Fix long names for instances #6070
Changed naming for instances to use only final part of the `folderPath`.
___
Traypublisher & Chore: Instance version on follow workfile version #6117
If `follow_workfile_version` is enabled but context does not have filled workfile version, a version on instance is used instead.
___
Substance Painter: Thumbnail errors with PBR Texture Set #6127
When publishing with PBR Metallic Roughness as Output Template, Emissive Map errors out because of the missing channel in the material and the map can't be generated in Substance Painter. This PR is to make sure `imagestance.data["publish"] = False` so that the related "empty" texture instance would be skipped to generate the output.
___
Transcoding: Fix reading image sequences through oiiotool #6129
When transcoding image sequences, the second image onwards includes the invalid xml line of `Reading path/to/file.exr` of the oiiotool output.This is most likely not the best solution, but it fixes the issue and illustrates the problem.Error:
```
ERROR:pyblish.plugin:Traceback (most recent call last):
File "C:\Users\tokejepsen\AppData\Local\Ynput\AYON\dependency_packages\ayon_2310271602_windows.zip\dependencies\pyblish\plugin.py", line 527, in __explicit_process
runner(*args)
File "C:\Users\tokejepsen\OpenPype\openpype\plugins\publish\extract_color_transcode.py", line 152, in process
File "C:\Users\tokejepsen\OpenPype\openpype\lib\transcoding.py", line 1136, in convert_colorspace
input_info = get_oiio_info_for_input(input_path, logger=logger)
File "C:\Users\tokejepsen\OpenPype\openpype\lib\transcoding.py", line 124, in get_oiio_info_for_input
output.append(parse_oiio_xml_output(xml_text, logger=logger))
File "C:\Users\tokejepsen\OpenPype\openpype\lib\transcoding.py", line 276, in parse_oiio_xml_output
tree = xml.etree.ElementTree.fromstring(xml_string)
File "xml\etree\ElementTree.py", line 1347, in XML
xml.etree.ElementTree.ParseError: syntax error: line 1, column 0
Traceback (most recent call last):
File "C:\Users\tokejepsen\AppData\Local\Ynput\AYON\dependency_packages\ayon_2310271602_windows.zip\dependencies\pyblish\plugin.py", line 527, in __explicit_process
runner(*args)
File "AYON: Remove 'IntegrateHeroVersion' conversion #6130
Remove settings conversion for `IntegrateHeroVersion`.
___
Chore tools: Make sure style object is not garbage collected #6136
Minor fix in tool utils to make sure style C++ object is not garbage collected when not stored into variable.
___
Maya: Apply initial viewport shader for Redshift Proxy after loading #6102
When the published redshift proxy is being loaded, the shader of the proxy is missing. This is different from the manual load through creating redshift proxy for files. This PR is to assign the default lambert to the redshift proxy, which replicates the same approach when the user manually loads the proxy with filepath.
___
General: We should keep current subset version when we switch only the representation type #4629
When we switch only the representation type of subsets, we should not get the representation from the last version of the subset.
___
Houdini: Add loader for redshift proxy family #5948
Loader for Redshift Proxy in Houdini (Thanks for @BigRoy contribution)
___
AfterEffects: exposing Deadline pools fields in Publisher UI #6079
Deadline pools might be adhoc set by an artist during publishing. AfterEffects implementation wasn't providing this.
___
Chore: Event callbacks can have order #6080
Event callbacks can have order in which are called, and fixed issue with getting function name and file when using `partial` function as callback.
___
AYON: OpenPype addon defines runtime dependencies #6095
Moved runtime dependencies from ayon-launcher to openpype addon.
___
Max: User's setting for scene unit scale #6097
Options for users to set the default scene unit scale for their scenes.AYONLegacy OP
___
Chore: Remove deprecated templates profiles #6103
Remove deprecated usage of template profiles from settings.
___
Publisher: Window is not always on top #6107
Goal of this PR is to avoid using `WindowStaysOnTopHint` which causes issues, especially in cases when DCC shows a popup dialog that is behind the window, in that case both Publisher and DCC are frozen and there is nothing to do.
___
Houdini: add split job export support for Redshift ROP #6108
This is adding support for splitting of export and render jobs for Redshift as is already implemented for Vray, Mantra and Arnold.
___
Fusion: automatic installation of PySide2 #6111
This PR adds hook which tries to check if PySide2 is installed in Python used by Fusion and if not, it tries to install it automatically.
___
AYON: OpenPype addon dependencies #6113
Added `click` and `six` to requirements of openpype addon, and removed `Qt.py` requirement, which is not used anywhere.
___
Chore: Thumbnail representation has 'outputName' #6114
Add thumbnail output name to thumbnail representation to prevent same output filename during integration.
___
Kitsu: Clear credentials is safe #6116
Do not remove not existing keyring items.
___
Maya: bug fix the playblast without textures #5942
Bug fix the texture not being displayed when users enable texture placement in the OP/AYON setting
___
Blender: Workfile instance update fix #6048
Make sure workfile instance has always available 'instance_node' in transient data.
___
Publisher: Fix issue with parenting of widgets #6106
Don't use publisher window parent (usually main DCC window) as parent for report widget.
___
:wrench: fix and update pydocstyle configuration #6109
Fix pydocstyle configuration and move it to `pyproject.toml`
___
Nuke: Create camera node with the latest camera node class in Nuke 14 #6118
Creating instance fails for certain cameras, and it seems to only exist in Nuke 14. The reason of causing that contributes to the new camera node class `Camera4` while the camera creator is working with the `Camera2` class.
___
Site Sync: small fixes in Loader #6119
Resolves issue:
- local and studio icons were same, they should be different
- `TypeError: string indices must be integers` error when downloading/uploading workfiles
___
Chore: Template data for editorial publishing #6120
Template data for editorial publishing are filled during `CollectInstanceAnatomyData`. The structure for editorial is determined, as it's required for ExtractHierarchy AYON/OpenPype plugins.
___
SceneInventory: Fix site sync icon conversion #6123
Use 'get_qt_icon' to convert icon definitions from site sync.
___
Testing: Release Maya/Deadline job from pending when testing. #5988
When testing we wont put the Deadline jobs into pending with dependencies, so the worker can start as soon as possible.
___
Max: Tweaks on Extractions for the exporters #5814
With this PR
- Suspend Refresh would be introduced in abc & obj extractors for optimization.
- Allow users to choose the custom attributes to be included in abc exports
___
Maya: Optional preserve references. #5994
Optional preserve references when publishing Maya scenes.
___
AYON ftrack: Expect 'ayon' group in custom attributes #6066
Expect `ayon` group as one of options to get custom attributes.
___
AYON Chore: Remove dependencies related to separated addons #6074
Removed dependencies from openpype client pyproject.toml that are already defined by addons which require them.
___
Editorial & chore: Stop using pathlib2 #6075
Do not use `pathlib2` which is Python 2 backport for `pathlib` module in python 3.
___
Traypublisher: Correct validator label #6084
Use correct label for Validate filepaths.
___
Nuke: Extract Review Intermediate disabled when both Extract Review Mov and Extract Review Intermediate disabled in setting #6089
Report in Discord https://discord.com/channels/517362899170230292/563751989075378201/1187874498234556477
___
Maya: Bug fix the file from texture node not being collected correctly in Yeti Rig #5990
Fix the bug of collect Yeti Rig not being able to get the file parameter(s) from the texture node(s), resulting to the failure of publishing the textures to the resource directory.
___
Bug: fix AYON settings for Maya workspace #6069
This is changing bug in default AYON setting for Maya workspace, where missing semicolumn caused workspace not being set. This is also syncing default workspace settings to OpenPype
___
Refactor colorspace handling in CollectColorspace plugin #6033
Traypublisher is now capable set available colorspaces or roles to publishing images sequence or video. This is fix of new implementation where we allowed to use roles in the enumerator selector.
___
Bugfix: Houdini render split bugs #6037
This PR is a follow up PR to https://github.com/ynput/OpenPype/pull/5420This PR does:
- refactor `get_output_parameter` to what is used to be.
- fix a bug with split render
- rename `exportJob` flag to `split_render`
___
Fusion: fix for single frame rendering #6056
Fixes publishes of single frame of `render` product type.
___
Photoshop: fix layer publish thumbnail missing in loader #6061
Thumbnails from any products (either `review` nor separate layer instances) weren't stored in Ayon.This resulted in not showing them in Loader and Server UI. After this PR thumbnails should be shown in the Loader and on the Server (`http://YOUR_AYON_HOSTNAME:5000/projects/YOUR_PROJECT/browser`).
___
AYON Chore: Do not use thumbnailSource for thumbnail integration #6063
Do not use `thumbnailSource` for thumbnail integration.
___
Photoshop: fix creation of .mov #6064
Generation of .mov file with 1 frame per published layer was failing.
___
Photoshop: fix Collect Color Coded settings #6065
Fix for wrong default value for `Collect Color Coded Instances` Settings
___
Bug: Fix Publisher parent window in Nuke #6067
Fixing issue where publisher parent window wasn't set because wrong use of version constant.
___
Python console widget: Save registry fix #6076
Do not save registry until there is something to save.
___
Ftrack: update asset names for multiple reviewable items #6077
Multiple reviewable assetVersion components with better grouping to asset version name.
___
Ftrack: DJV action fixes #6098
Fix bugs in DJV ftrack action.
___
AYON Workfiles tool: Fix arrow to timezone typo #6099
Fix parenthesis typo with arrow local timezone function.
___
Chore: Update folder-favorite icon to ayon icon #5718
Updates old "Pype-2.0-era" (from ancient greece times) to AYON logo equivalent.I believe it's only used in Nuke.
___
Chore: Maya / Nuke remove publish gui filters from settings #5570
- Remove Publish GUI Filters from Nuke settings
- Remove Publish GUI Filters from Maya settings
___
Fusion: Project/User option for output format (create_saver) #6045
Adds "Output Image Format" option which can be set via project settings and overwritten by users in "Create" menu. This replaces the current behaviour of being hardcoded to "exr". Replacing the need for people to manually edit the saver path if they require a different extension.
___
Fusion: Output Image Format Updating Instances (create_saver) #6060
Adds the ability to update Saver image output format if changed in the Publish UI.~~Adds an optional validator that compares "Output Image Format" in the Publish menu against the one currently found on the saver. It then offers a repair action to update the output extension on the saver.~~
___
Tests: Fix representation count for AE legacy test #6072
___
AYON: Update ayon api to 1.0.0-rc.3 #6052
Updated ayon python api to 1.0.0-rc.3.
___
Chore: Fix subst paths handling #5702
Make sure that source disk ends with `\` instead of destination disk.
___
AYON: Use folder path as unique identifier #5817
Use folder path instead of asset name as unique identifier, with OpenPype compatibility.
___
Houdini: Farm caching submission to Deadline #4903
Implements functionality to offload instances of the specific families to be processed on Deadline instead of locally. This increases productivity as artist can use local machine could be used for other tasks.Implemented for families:
- [x] ass
- [x] redshift proxy
- [x] ifd
- [x] abc
- [x] bgeo
- [x] vdb
___
Houdini: Add support to split Deadline render tasks in export + render #5420
This adds initial support in Houdini so when submitting render jobs to Deadline it's not running as a single Houdini task but rather it gets split in two different tasks: Export + Render. This way it's more efficient as we only need a Houdini license during the export step and the render tasks can run exclusively with a render license. Moreover, we aren't wasting all the overhead time of opening the render scene in Houdini for every frame.I have also added the corresponding settings json files so we can set some of the default values for the Houdini deadline submitter.
___
Wrap: new integration #5823
These modifications are necessary for adding Wrap integration (DCC handling scans and textures) .
___
AYON: Prepare for 'data' via graphql #5923
AYON server does support to query 'data' field for hierarchy entities (project > ... > representation) using GraphQl since version 0.5.5. Because of this PR in ayon-python-api it is required to modify custom graphql function in `openpype.client` to support that option.
___
Chore AYON: AYON addon class #5937
Introduced base class for AYON addon in openpype modules discovery logic.
___
Asset Usage Reporter Tool #5946
This adds simple tool for OpenPype mode that will go over all published workfiles and print linked assets and their version:This is created per project and can be exported in csv file or copied to clipboard in _"ASCII Human readable form"_.
___
Testing: dump_databases flag #5955
This introduces a `dump_databases` flag which makes it convenient to output the resulting database of a successful test run. The flag supports two formats; `bson` and `json`.Due to outputting to the test data folder, when dumping the databases, the test data folder will persist.Split from https://github.com/ynput/OpenPype/pull/5644
___
SiteSync: implemented in Ayon Loader #5962
Implemented `Availability` column in Ayon loader and redo of loaders to `ActionItems` in representation window there.
___
AYON: Workfile template build works #5975
Modified workfile template builder to work, to some degree, in AYON mode.
___
Maya: Small Tweaks on Validator for Look Default Shader Connection for Maya 2024 #5957
Resolve https://github.com/ynput/OpenPype/issues/5269
___
Settings: Changes in default settings #5983
We've made some changes in the default settings as several application versions were obsolete (Maya 18, Nuke 11, PS 2020, etc). Also added tools and changed settings for Blender, Maya, and Blender.
All should work as usual.
___
Testing: Do not persist data by default in Maya/Deadline. #5987
This is similar to the Maya publishing test.
___
Max: Validate loaded plugins tweaks #5820
In the current development of 3dsMax, users need to use separate validators to validate if certain plugins being loaded before the extraction. For example, usd extractor in model family, prt/tycache extractor in pointcloud/tycache family.But with the PR where implements optional validate loaded plugin, users just need to put what kind of plugins they want to validate in the settings. They no longer need to go through all the separate plugin validators when publishing, and only one validator would do all the check on the loaded plugins before extraction.
___
Nuke: Change context label enhancement #5887
Use QAction to change label of context label in Nuke pipeline menu.
___
Chore: Do not use template data as source for context #5918
Use available information on context to receive context data instead of using `"anatomyData"` during publishing.
___
Houdini: Add python3.10 libs for Houdini 20 startup #5932
Add python3.10 libs for Houdini 20 startup
___
General: Use colorspace data when creating thumbnail #5938
Thumbnails with applied colormanagement.
___
Ftrack: rewriting component creation to support multiple thumbnails #5939
The creation of Ftrack components needs to allow for multiple thumbnails. This is important in situations where there could be several reviewable streams, like in the case of a nuke intermediate files preset. Customers have asked for unique thumbnails for each data stream.For instance, one stream might contain a baked LUT file along with Display and View. Another stream might only include the baked Display and View. These variations can change the overall look. Thus, we found it necessary to depict these differences via thumbnails.
___
Chore: PySide6 tree view style #5940
Define solid color for background of branch in QTreeView.
___
Nuke: Explicit Thumbnail workflow #5941
Nuke made a shift from using its own plugin to a global one for thumbnail creation. This was because it had to handle several thumbnail workflows for baking intermediate data streams. To manage this, the global plugin had to be upgraded. Now, each baking stream can set a unique tag 'need_thumbnail'. This tag is used to mark representations that need a thumbnail.
___
Global: extract thumbnail with new settings #5944
Settings are now configurable for the following:
- target size of thumbnail - source or constrained to specific
- where should be frame taken from in sequence or video file
- if thumbnail should be integrated or not
- background color for letter boxes
- added AYON settings
___
RoyalRender: inject submitter environment to the royal render job #5958
This is an attempt to solve runtime environment injection for render jobs in RoyalRender as there is no easy way to implement something like `GlobalJobPreload` logic in Deadline. Idea is to inject OpenPype environments directly to the job itself.
___
General: Use manual thumbnail if present when publishing #5969
Use manual thumbnail added to the publisher instead of using it from published representation.
___
AYON: Change of server url should work as expected #5971
Using login action in tray menu to change server url should correctly start new process without issues of missing bundle or previous url.
___
AYON: make sure the AYON menu bar in 3dsMax is named AYON when AYON launches #5972
Renaming the menu bar in 3dsMax for AYON and some cosmetic fix in the docstring
___
Resolve: renaming menu to AYON #5974
Resolve in Ayon is now having aligned name.
___
Hiero: custom tools menu rename #5976
- OpenPype Tools are now Custom Tools menu
- fixing order of tools. Create should be first.
___
nuke: updating name for custom tools menu item #5977
- Ayon variant of settings renamed `Custom Tools` menu item
___
fusion: AYON renaming menu #5978
Fusion is having Ayon menu.
___
Blender: Changed the labels for Layout JSON Extractor #5981
Changed the labels for Blender's Layout JSON Extractor.
___
Testing: Skip Arnold license for test rendering. #5984
Skip license check when rendering for testing.
___
Testing: Validate errors and failed status from Deadline jobs. #5986
While waiting for the Deadline jobs to finish, we query the errors on the job and its dependent jobs to fail as early as possible. Plus the failed status.
___
AYON: rename Openpype Tools as Custom Tools in Maya Host #5991
Rename Openpype Tools as Custom Tools in Maya Host in
___
AYON: Use AYON label in ayon mode #5995
Replaced OpenPype with AYON in AYON mode and added bundle nam to information.
___
AYON: Update ayon python api #6002
Updated ayon-python-api to '1.0.0-rc.1'.
___
Max: Add missing repair action in validate resolution setting #6014
Add missing repair action for validate resolution setting
___
Add the AYON/OP settings to enable extractor for model family in 3dsmax #6027
Add the AYON/OP settings to enable extractor for model family in 3dsmax
___
Bugfix: Fix error message formatting if ayon executable can't be found by deadline #6028
Without this fix the error message would report executables string with `;` between EACH character, similar to this PR: https://github.com/ynput/OpenPype/pull/5815However that PR apparently missed also fixing it in `GlobalJobPreLoad` and only fixed it in `Ayon.py` plugin.
___
Show slightly different info in AYON mode #6031
This PR changes what is shown in Tray menu in AYON mode. Previously, it showed version of OpenPype that is very confusing in AYON mode. So this now shows AYON version instead. When clicked, it will opene AYON info window, where OpenPype version is now added, for debugging purposes.
___
AYON Editorial: Hierarchy context have names as keys #6041
Use folder name as keys in `hierarchyContext` and modify hierachy extraction accordingly.
___
AYON: Convert the createAt value to local timezone #6043
Show correct create time in UIs.
___
Maya: Render creation - fix broken imports #5893
Maya specific imports were moved to specific methods but not in all cases by #5775. This is just quickly restoring functionality without questioning that decision.
___
Maya: fix crashing model renderset collector #5929
This fix is handling case where model is in some type of render sets but no other connections are made there. Publishing this model would fail with `RuntimeError: Found no items to list the history for.`
___
Maya: Remove duplicated attributes of MTOA verbosity level #5945
Remove duplicated attributes implementation mentioned in https://github.com/ynput/OpenPype/pull/5931#discussion_r1402175289
___
Maya: Bug fix Redshift Proxy not being successfully published #5956
Bug fix redshift proxy family not being successfully published due to the error found in integrate.py
___
Maya: Bug fix load image for texturesetMain #6011
Bug fix load image with file node for texturesetMain
___
Maya: bug fix the repair function in validate_rendersettings #6021
The following error has been encountered below:
```
// pyblish.pyblish.plugin.Action : Finding failed instances..
// pyblish.pyblish.plugin.Action : Attempting repair for instance: renderLookdevMain ...
// Error: pyblish.plugin : Traceback (most recent call last):
// File "C:\Users\lbate\AppData\Local\Ynput\AYON\dependency_packages\ayon_2310271602_windows.zip\dependencies\pyblish\plugin.py", line 527, in __explicit_process
// runner(*args)
// File "C:\Users\lbate\AppData\Local\Ynput\AYON\addons\openpype_3.17.7-nightly.6\openpype\pipeline\publish\publish_plugins.py", line 241, in process
// plugin.repair(instance)
// File "C:\Users\lbate\AppData\Local\Ynput\AYON\addons\openpype_3.17.7-nightly.6\openpype\hosts\maya\plugins\publish\validate_rendersettings.py", line 395, in repair
// cmds.setAttr("{}.{}".format(node, prefix_attr),
// UnboundLocalError: local variable 'node' referenced before assignment
// Traceback (most recent call last):
// File "C:\Users\lbate\AppData\Local\Ynput\AYON\dependency_packages\ayon_2310271602_windows.zip\dependencies\pyblish\plugin.py", line 527, in __explicit_process
// runner(*args)
// File "C:\Users\lbate\AppData\Local\Ynput\AYON\addons\openpype_3.17.7-nightly.6\openpype\pipeline\publish\publish_plugins.py", line 241, in process
// plugin.repair(instance)
// File "C:\Users\lbate\AppData\Local\Ynput\AYON\addons\openpype_3.17.7-nightly.6\openpype\hosts\maya\plugins\publish\validate_rendersettings.py", line 395, in repair
// cmds.setAttr("{}.{}".format(node, prefix_attr),
// UnboundLocalError: local variable 'node' referenced before assignment
```
This PR is a fix for that
___
Fusion: Render avoid unhashable type `BlackmagicFusion.PyRemoteObject` error #5672
Fix Fusion 18.6+ support: Avoid issues with Fusion's `BlackmagicFusion.PyRemoteObject` instances being unhashable.
```python
Traceback (most recent call last):
File "E:\openpype\OpenPype\.venv\lib\site-packages\pyblish\plugin.py", line 527, in __explicit_process
runner(*args)
File "E:\openpype\OpenPype\openpype\hosts\fusion\plugins\publish\extract_render_local.py", line 61, in process
result = self.render(instance)
File "E:\openpype\OpenPype\openpype\hosts\fusion\plugins\publish\extract_render_local.py", line 118, in render
with enabled_savers(current_comp, savers_to_render):
File "C:\Users\User\AppData\Local\Programs\Python\Python39\lib\contextlib.py", line 119, in __enter__
return next(self.gen)
File "E:\openpype\OpenPype\openpype\hosts\fusion\plugins\publish\extract_render_local.py", line 33, in enabled_savers
original_states[saver] = original_state
TypeError: unhashable type: 'BlackmagicFusion.PyRemoteObject'
```
___
Nuke: Validate Nuke Write Nodes refactor to use variable `node_value` instead of `value` #5764
Nuke: Validate Nuke Write Nodes refactor to use variable `node_value` instead of `value`The variable `value` only exists as the last variable value in the `for value in values` loop and might not be declared if `values` is an empty iterable.
___
resolve: fixing loader handles calculation #5863
Resolve was not correctly calculating duration of database related duration.
___
Chore: Staging mode determination #5895
Resources use `is_staging_enabled` function instead of `is_running_staging` to determine if should use staging icon. And fixed comparison bug in `is_running_staging`.
___
AYON: Handle staging templates category #5905
Staging anatomy templates category is handled during project templates conversion. The keys are stored into `others` with `"staging_"` prefix.
___
Max: fix the subset name not changing accordingly after the variant name changes #5911
Resolve #5902
___
AYON: Loader tool bugs hunt #5915
Fix issues with invalid representation ids in loaded containers and handle missing product type in server database.
___
Publisher: Bugfixes and enhancements #5924
Small fixes/enhancements in publisher UI.
___
Maya: Supports for additional Job Info and Plugin Info in deadline submission #5931
This PR is to resolve some of the attributes such as MTOA's `ArnoldVerbose` are not preserved on farm and users can use the project settings to add the attributes back to either job or plugin Info.
___
Bugfix: Houdini license validator missing families #5934
Adding missing families to Houdini license validator.
___
TrayPublisher: adding back `asset_doc` variable #5943
Returning variable which had been removed accidentally in previous PR.
___
Settings: Fix ModulesManager init args #5947
Remove usage of kwargs to create ModulesManager.
___
Blender: Fix Deadline Frames per task #5949
Fixed a problem with Frames per task setting not being applied when publishing a render.
___
Testing: Fix is_test_failed #5951
`is_test_failed` is used (exclusively) on module fixtures to determine whether the tests have failed or not. This determines whether to run tear down code like cleaning up the database and temporary files.But in the module scope `request.node.rep_call` is not available, which results in `is_test_failed` always returning `True`, and no tear down code get executed.The solution was taken from; https://github.com/pytest-dev/pytest/issues/5090
___
Harmony: Fix local rendering #5953
Local rendering was throwing warning about license, but didn't fail per se. It just didn't produce anything.
___
Testing: hou module should be within class code. #5954
`hou` module should be within the class code else we'll get pyblish errors from needing to skip the plugin.
___
Maya: Add Label to MayaUSDReferenceLoader #5964
As the create placeholder dialog displays the two distinct loaders with the same name, this PR is to distinguish Maya USD Reference Loaders from the loaders of which inherited from. See the screenshot below:
___
Max: Bug fix the resolution not being shown correctly in review burnin #5965
The resolution is not being shown correctly in review burnin
___
AYON: Fix thumbnail integration #5970
Thumbnail integration could cause crash of server if thumbnail id was changed for the same entity id multiple times. Modified the code to avoid that issue.
___
Photoshop: Updated label in Settings #5980
Replaced wrong label from different plugin.
___
Photoshop: Fix removed unsupported Path #5996
Path is not json serializable by default, it is not necessary, better model reused.
___
AYON: Prepare functions for newer ayon-python-api #5997
Newer ayon python api will add new filtering options or change order of existing. Kwargs are used in client code to prevent issues on update.
___
AYON: Conversion of the new playblast settings in Maya #6000
Conversion of the new playblast settings in Maya
___
AYON: Bug fix for loading Mesh in Substance Painter as new project not working #6004
Substance Painter in AYON can't load mesh for creating a new project
___
Deadline: correct webservice couldn't be selected in Ayon #6007
Changed the Setting model to mimic more OP approach as it needs to live together for time being.
___
AYON tools: Fix refresh thread #6008
Trigger 'refresh_finished' signal out of 'run' method.
___
Ftrack: multiple reviewable components missing variable #6013
Missing variable in code for editorial publishing in traypublisher.
___
TVPaint: Expect legacy instances in metadata #6015
Do not expect `"workfileInstances"` constains only new type instance data with `creator_identifier`.
___
Bugfix: handle missing key in Deadline #6019
This quickly fixes bug introduced by #5420
___
Revert `extractenvironments` behaviour #6020
This is returning original behaviour of `extractenvironments` command from before #5958 so we restore functionality.
___
OP-7535 - Fix renaming composition in AE #6025
Removing of `render` instance caused renaming of composition to `dummyComp` which caused issue in publishing in next attempt.This PR stores original composition name(cleaned up for product name creation) and uses it if instance needs to be removed.
___
Refactor code to skip instance creation for new assets #6029
Publishing effects from hiero during editorial publish is working as expected again.
___
Refactor code to handle missing "representations" key in instance data #6032
Minor code change for optimisation of thumbnail workflow.
___
Traypublisher: editorial preserve clip case sensitivity #6036
Keep EDL clip name inheritance with case sensitivity.
___
Bugfix/add missing houdini settings #6039
add missing settings. now, it looks like this:| Ayon | OpenPype || -- | -- | | | || | |
___
Maya: Remove RenderSetup layer observers #5836
Remove RenderSetup layer observers that are not needed since new publisher since Renderlayer Creators manage these themselves on Collect and Save/Update of instances.
___
Tests: Removed render instance #6026
This test was created as simple model and workfile publish, without Deadline rendering. Cleaned up render elements.
___
Tests: update after thumbnail default change #6040
https://github.com/ynput/OpenPype/pull/5944 changed default state of integration of Thumbnails to NOT integrate. This PR updates automatic tests to follow that.
___
Houdini: Remove legacy LOPs USD output processors #5861
Remove unused/broken legacy code for Houdini Solaris USD LOPs output processors. The code was originally written in Avalon, against early Houdini 18 betas which had a different API for output processors and thus the current state doesn't even work in recent versions of Houdini.
___
Chore: Substance Painter Addons for Ayon #5914
Substance Painter Addons for Ayon
___
Ayon: Updated name of Adobe extension to Ayon #5992
This changes name in menu in Adobe extensions to Ayon.
___
Chore/houdini update startup log #6003
print `Installing AYON ...` on startup when launching houdini from launcher in ayon mode.also update submenu to `ayon_menu` instead of `openpype_menu`
___
Revert "Ayon: Updated name of Adobe extension to Ayon" #6010
Reverts ynput/OpenPype#5992
That PR is only applicable to Ayon.
___
Standalone/Tray Publisher: Remove simple Unreal texture publishing #6012
We are removing _simple Unreal Texture publishing_ that was just renaming texture files to fit to Unreal naming conventions but without any additional functionality. We might return this functionality back with better texture publishing system.Related to #5983
___
Deadline: Bump version because of Settings changes for Deadline #6023
___
Change ASCII art in the Console based on the server mode #6030
This changes ASCII art in the console based on the AYON/OpenPype mode
___
Testing: Validate Maya Logs #5775
This PR adds testing of the logs within Maya such as Python and Pyblish errors.The reason why we need to touch so many files outside of Maya is because of the pyblish errors below;
```
pyblish (ERROR) (line: 1371) pyblish.plugin:
Skipped: "collect_otio_frame_ranges" (No module named 'opentimelineio')
# Error: pyblish.plugin : Skipped: "collect_otio_frame_ranges" (No module named 'opentimelineio') #
pyblish (ERROR) (line: 1371) pyblish.plugin:
Skipped: "collect_otio_review" (No module named 'opentimelineio')
# Error: pyblish.plugin : Skipped: "collect_otio_review" (No module named 'opentimelineio') #
pyblish (ERROR) (line: 1371) pyblish.plugin:
Skipped: "collect_otio_subset_resources" (No module named 'opentimelineio')
# Error: pyblish.plugin : Skipped: "collect_otio_subset_resources" (No module named 'opentimelineio') #
pyblish (ERROR) (line: 1371) pyblish.plugin:
Skipped: "extract_otio_audio_tracks" (No module named 'opentimelineio')
# Error: pyblish.plugin : Skipped: "extract_otio_audio_tracks" (No module named 'opentimelineio') #
pyblish (ERROR) (line: 1371) pyblish.plugin:
Skipped: "extract_otio_file" (No module named 'opentimelineio')
# Error: pyblish.plugin : Skipped: "extract_otio_file" (No module named 'opentimelineio') #
pyblish (ERROR) (line: 1371) pyblish.plugin:
Skipped: "extract_otio_review" (No module named 'opentimelineio')
# Error: pyblish.plugin : Skipped: "extract_otio_review" (No module named 'opentimelineio') #
pyblish (ERROR) (line: 1371) pyblish.plugin:
Skipped: "extract_otio_trimming_video" (No module named 'opentimelineio')
# Error: pyblish.plugin : Skipped: "extract_otio_trimming_video" (No module named 'opentimelineio') #
pyblish (ERROR) (line: 1371) pyblish.plugin:
Skipped: "submit_blender_deadline" (No module named 'bpy')
# Error: pyblish.plugin : Skipped: "submit_blender_deadline" (No module named 'bpy') #
pyblish (ERROR) (line: 1371) pyblish.plugin:
Skipped: "submit_houdini_remote_publish" (No module named 'hou')
# Error: pyblish.plugin : Skipped: "submit_houdini_remote_publish" (No module named 'hou') #
pyblish (ERROR) (line: 1371) pyblish.plugin:
Skipped: "submit_houdini_render_deadline" (No module named 'hou')
# Error: pyblish.plugin : Skipped: "submit_houdini_render_deadline" (No module named 'hou') #
pyblish (ERROR) (line: 1371) pyblish.plugin:
Skipped: "submit_max_deadline" (No module named 'pymxs')
# Error: pyblish.plugin : Skipped: "submit_max_deadline" (No module named 'pymxs') #
pyblish (ERROR) (line: 1371) pyblish.plugin:
Skipped: "submit_nuke_deadline" (No module named 'nuke')
# Error: pyblish.plugin : Skipped: "submit_nuke_deadline" (No module named 'nuke') #
```
We also needed to `stdout` and `stderr` from the launched application to capture the output.Split from #5644.Dependent on #5734
___
Maya: Render Settings cleanup remove global `RENDER_ATTRS` #5801
Remove global `lib.RENDER_ATTRS` and implement a `RenderSettings.get_padding_attr(renderer)` method instead.
___
Testing: Ingest expected files and input workfile #5840
This ingests the Maya workfile from the Drive storage. Have changed the format to MayaAscii so its easier to see what changes are happening in a PR. This meant changing the expected files and database entries as well.
___
Chore: Create plugin auto-apply settings #5908
Create plugins can auto-apply settings.
___
Resolve: Add save current file button + "Save" shortcut when menu is active #5691
Adds a "Save current file" to the OpenPype menu.Also adds a "Save" shortcut key sequence (CTRL+S on Windows) to the button, so that clicking CTRL+S when the menu is active will save the current workfile. However this of course does not work if the menu does not receive the key press event (e.g. when Resolve UI is active instead)Resolves #5684
___
Reference USD file as maya native geometry #5781
Add MayaUsdReferenceLoader to reference USD as Maya native geometry using `mayaUSDImport` file translator.
___
Max: Bug fix on wrong aspect ratio and viewport not being maximized during context in review family #5839
This PR will fix the bug on wrong aspect ratio and viewport not being maximized when creating preview animationBesides, the support of tga image format and the options for AA quality are implemented in this PR
___
Blender: Incorporate blender "Collections" into Publish/Load #5841
Allow `blendScene` family to include collections.
___
Max: Allows user preset the setting of preview animation in OP/AYON Setting #5859
Allows user preset the setting of preview animation in OP/AYON Setting for review family.
- [x] Openpype
- [x] AYON
___
Publisher: Center publisher window on first show #5877
Move publisher window to center of a screen on first show.
___
Publisher: Instance context changes confirm works #5881
Confirmation of context changes in publisher on existing instances does not cause glitches.
___
AYON workfiles tools: Revisit workfiles tool #5897
Revisited workfiles tool for AYON mode to reuse common models and widgets.
___
Nuke: updated colorspace settings #5906
Updating nuke colorspace settings into more convenient way with usage of ocio config roles rather then particular colorspace names. This way we should not have troubles to switch between linear Rec709 or ACES configs without any additional settings changes.
___
Blender: Refactor to new publisher #5910
Refactor Blender integration to use the new publisher
___
Enhancement: Some publish logs cosmetics #5917
General logging message tweaks:
- Sort some lists of folder/filenames so they appear sorted in the logs
- Fix some grammar / typos
- In some cases provide slightly more information in a log
___
Blender: Better name of 'asset_name' function #5927
Renamed function `asset_name` to `prepare_scene_name`.
___
Maya: Bug fix the fbx animation export errored out when the skeletonAnim set is empty #5875
Resolve this bug discordIf the skeletonAnim SET is empty and fbx animation collect, the fbx animation extractor would skip the fbx extraction
___
Bugfix: fix few typos in houdini's and Maya's Ayon settings #5882
Fixing few typos
- [x] Maya unreal static mesh
- [x] Houdini static mesh
- [x] Houdini collect asset handles
___
Bugfix: Ayon Deadline env vars + error message on no executable found #5815
Fix some Ayon x Deadline issues as came up in this topic:
- missing Environment Variables issue explained here for `deadlinePlugin.RunProcess` for the AYON _extract environments_ call.
- wrong error formatting described here with a `;` between each character like this: `Ayon executable was not found in the semicolon separated list "C;:;/;P;r;o;g;r;a;m; ;F;i;l;e;s;/;Y;n;p;u;t;/;A;Y;O;N; ;1;.;0;.;0;-;b;e;t;a;.;5;/;a;y;o;n;_;c;o;n;s;o;l;e;.;e;x;e". The path to the render executable can be configured from the Plugin Configuration in the Deadline Monitor.`
___
AYON: Fix bundles access in settings #5856
Fixed access to bundles data in settings to define correct develop variant.
___
AYON 3dsMax settings: 'ValidateAttributes' settings converte only if available #5878
Convert `ValidateAttributes` settings only if are available in AYON settings.
___
AYON: Fix TrayPublisher editorial settings #5880
Fixing Traypublisher settings for adding task in simple editorial.
___
TrayPublisher: editorial frame range check not needed #5884
Validator for frame ranges is not needed during editorial publishing since entity data are not yet in database.
___
Update houdini license validator #5886
As reported in this community commentHoudini USD publishing is only restricted in Houdini apprentice.
___
Blender: Fix blend extraction and packed images #5888
Fixed a with blend extractor and packed images.
___
AYON: Initialize connection with all information #5890
Create global AYON api connection with all informations all the time.
___
AYON: Scene inventory tool without site sync #5896
Skip 'get_site_icons' if site sync addon is disabled.
___
Publish report tool: Fix PySide6 #5898
Use constants from classes instead of objects.
___
fusion: removing hardcoded template name for saver #5907
Fusion is not hardcoded for `render` anatomy template only anymore. This was blocking AYON deployment.
___
Fusion: Add USD loader #4896
Add an OpenPype managed USD loader (`uLoader`) for Fusion.
___
Fusion: Resolution validator #5325
Added a resolution validator.The code is from my old PR (https://github.com/ynput/OpenPype/pull/4921) that I closed because the PR also contained a frame range validator that no longer is needed.
___
Context Selection tool: Refactor Context tool (for AYON) #5766
Context selection tool has AYON variant.
___
AYON: Use AYON username for user in template data #5842
Use ayon username for template data in AYON mode.
___
Testing: app_group flag #5869
`app_group` command flag. This is for changing which flavour of the host to launch. In the case of Maya, you can launch Maya and MayaPy, but it can be used for the Nuke family as well.Split from #5644
___
Enhancement: Fusion fix saver creation + minor Blender/Fusion logging tweaks #5558
- Blender change logs to `debug` level in preparation for new publisher artist facing reports (note that it currently still uses the old publisher)
- Fusion: Create Saver fix redeclaration of default_variants
- Fusion: Fix saver being created in incorrect state without saving directly after create
- Fusion: Allow reset frame range on render family
- Fusion: Tweak logging level for artist-facing report
___
Resolve: load clip to timeline at set time #5665
It is possible to load clip to correct place on timeline.
___
Nuke: Optional Deadline workfile dependency. #5732
Adds option to add the workfile as dependency for the Deadline job.Think it used to have something like this, but it disappeared. Usecase is for remote workflow where the Nuke script needs to be synced before the job can start.
___
Enhancement/houdini rearrange ayon houdini settings files #5748
Rearranging Houdini Settings to be more readable, easier to edit, update settings (include all families/product types)This PR is mainly for Ayon Settings to have more organized files. For Openpype, I'll make sure that each Houdini setting in Ayon has an equivalent in Openpype.
- [x] update Ayon settings, fix typos and remove deprecated settings.
- [x] Sync with Openpype
- [x] Test in Openpype
- [x] Test in Ayon
___
Chore: updating create ayon addon script #5822
Adding developers environment options.
___
Max: Implement Validator for Properties/Attributes Value Check #5824
Add optional validator which can check if the property attributes are valid in Max
___
Nuke: Remove unused 'get_render_path' function #5826
Remove unused function `get_render_path` from nuke integration.
___
Chore: Limit current context template data function #5845
Current implementation of `get_current_context_template_data` does return the same values as base template data function `get_template_data`.
___
Max: Make sure Collect Render not ignoring instance asset #5847
- Make sure Collect Render is not always using asset from context.
- Make sure Scene version being collected
- Clean up unnecessary uses of code in the collector.
___
Ftrack: Events are not processed if project is not available in OpenPype #5853
Events that happened on project which is not in OpenPype is not processed.
___
Nuke: Add Nuke 11.0 as default setting #5855
Found I needed Nuke 11.0 in the default settings to help with unit testing.
___
TVPaint: Code cleanup #5857
Removed unused import. Use `AYON` label in ayon mode. Removed unused data in publish context `"previous_context"`.
___
AYON settings: Use correct label for follow workfile version #5874
Follow workfile version label was marked as Collect Anatomy Instance Data label.
___
Nuke: Fix workfile template builder so representations get loaded next to each other #5061
Refactor when the cleanup of the placeholder happens for the cases where multiple representations are loaded by a single placeholder.The existing code didn't take into account the case where a template placeholder can load multiple representations so it was trying to do the cleanup of the placeholder node and the re-arrangement of the imported nodes too early. I assume this was designed only for the cases where a single representation can load multiple nodes.
___
Nuke: Dont update node name on update #5704
When updating `Image` containers the code is trying to set the name of the node. This results in a warning message from Nuke shown below;Suggesting to not change the node name when updating.
___
UIDefLabel can be unique #5827
`UILabelDef` have implemented comparison and uniqueness.
___
AYON: Skip kitsu module when creating ayon addons #5828
Create AYON packages is skipping kitsu module in creation of modules/addons and kitsu module is not loaded from modules on start. The addon already has it's repository https://github.com/ynput/ayon-kitsu.
___
Bugfix: Collect Rendered Files only collecting first instance #5832
Collect all instances from the metadata file - don't return on first instance iteration.
___
Houdini: set frame range for the created composite ROP #5833
Quick bug fix for created composite ROP, set its frame range to the frame range of the playbar.
___
Fix registering launcher actions from OpenPypeModules #5843
Fix typo `actions_dir` -> `path` to fix register launcher actions fromm OpenPypeModule
___
Bugfix in houdini shelves manager and beautify settings #5844
This PR fixes the problem in this PR https://github.com/ynput/OpenPype/issues/5457 by using the right function to load a pre-made houdini `.shelf` fileAlso, it beautifies houdini shelves settings to provide better guidance for users which helps with other issue https://github.com/ynput/OpenPype/issues/5458 , Rather adding default shelf and set names, I'll educate users how to use the tool correctly.Users now are able to select between the two options.| OpenPype | Ayon || -- | -- || | |
___
Blender: Fix missing Grease Pencils in review #5848
Fix Grease Pencil missing in review when isolating objects.
___
Blender: Fix Render Settings in Ayon #5849
Fix Render Settings in Ayon for Blender.
___
Bugfix: houdini tab menu working as expected #5850
This PR:Tab menu name changes to Ayon when using ayon get_network_categories is checked in all creator plugins. | Product | Network Category | | -- | -- | | Alembic camera | rop, obj | | Arnold Ass | rop | | Arnold ROP | rop | | Bgeo | rop, sop | | composite sequence | cop2, rop | | hda | obj | | Karma ROP | rop | | Mantra ROP | rop | | ABC | rop, sop | | RS proxy | rop, sop| | RS ROP | rop | | Review | rop | | Static mesh | rop, obj, sop | | USD | lop, rop | | USD Render | rop | | VDB | rop, obj, sop | | V Ray | rop |
___
Bigfix: Houdini skip frame_range_validator if node has no 'trange' parameter #5851
I faced a bug when publishing HDA instance as it has no `trange` parameter. As this PR title says : skip frame_range_validator if node has no 'trange' parameter
___
Bugfix: houdini image sequence loading and missing frames #5852
I made this PR in to fix issues mentioned here https://github.com/ynput/OpenPype/pull/5833#issuecomment-1789207727in short:
- image load doesn't work
- publisher only publish one frame
___
Nuke: loaders' containers updating as nodes #5854
Nuke loaded containers are updating correctly even they have been duplicating of originally loaded nodes. This had previously been removed duplicated nodes.
___
deadline: settings are not blocking extension input #5864
Settings are not blocking user input.
___
Blender: Fix loading of blend layouts #5866
Fix a problem with loading blend layouts.
___
AYON: Launcher refresh issues #5867
Fixed refresh of projects issue in launcher tool. And renamed Qt models to contain `Qt` in their name (it was really hard to find out where were used). It is not possible to click on disabled item in launcher's projects view.
___
Fix the Wrong key words for tycache workfile template settings in AYON #5870
Fix the wrong key words for the tycache workfile template settings in AYON(i.e. Instead of families, product_types should be used)
___
AYON tools: Handle empty icon definition #5876
Ignore if passed icon definition is `None`.
___
Houdini: Remove on instance toggled callback #5860
Remove on instance toggled callback which isn't relevant to the new publisher
___
Chore: Remove unused `instanceToggled` callbacks #5862
The `instanceToggled` callbacks should be irrelevant for new publisher.
___
Add Support for Husk-AYON Integration #5816
This draft pull request introduces support for integrating Husk with AYON within the OpenPype repository.
___
Push to project tool: Prepare push to project tool for AYON #5770
Cloned Push to project tool for AYON and modified it.
___
Max: tycache family support #5624
Tycache family supports for Tyflow Plugin in Max
___
Unreal: Changed behaviour for updating assets #5670
Changed how assets are updated in Unreal.
___
Unreal: Improved error reporting for Sequence Frame Validator #5730
Improved error reporting for Sequence Frame Validator.
___
Max: Setting tweaks on Review Family #5744
- Bug fix of not being able to publish the preferred visual style when creating preview animation
- Exposes the parameters after creating instance
- Add the Quality settings and viewport texture settings for preview animation
- add use selection for create review
___
Max: Add families with frame range extractions back to the frame range validator #5757
In 3dsMax, there are some instances which exports the files in frame range but not being added to the optional frame range validator. In this PR, these instances would have the optional frame range validators to allow users to check if frame range aligns with the context data from DB.The following families have been added to have optional frame range validator:
- maxrender
- review
- camera
- redshift proxy
- pointcache
- point cloud(tyFlow PRT)
___
TimersManager: Use available data to get context info #5804
Get context information from pyblish context data instead of using `legacy_io`.
___
Chore: Removed unused variable from `AbstractCollectRender` #5805
Removed unused `_asset` variable from `RenderInstance`.
___
Bugfix/houdini: wrong frame calculation with handles #5698
This PR make collect plugins to consider `handleStart` and `handleEnd` when collecting frame range it affects three parts:
- get frame range in collect plugins
- expected file in render plugins
- submit houdini job deadline plugin
___
Nuke: ayon server settings improvements #5746
Nuke settings were not aligned with OpenPype settings. Also labels needed to be improved.
___
Blender: Fix pointcache family and fix alembic extractor #5747
Fixed `pointcache` family and fixed behaviour of the alembic extractor.
___
AYON: Remove 'shotgun_api3' from dependencies #5803
Removed `shotgun_api3` dependency from openpype dependencies for AYON launcher. The dependency is already defined in shotgrid addon and change of version causes clashes.
___
Chore: Fix typo in filename #5807
Move content of `contants.py` into `constants.py`.
___
Chore: Create context respects instance changes #5809
Fix issue with unrespected change propagation in `CreateContext`. All successfully saved instances are marked as saved so they have no changes. Origin data of an instance are explicitly not handled directly by the object but by the attribute wrappers.
___
Blender: Fix tools handling in AYON mode #5811
Skip logic in `before_window_show` in blender when in AYON mode. Most of the stuff called there happes on show automatically.
___
Blender: Include Grease Pencil in review and thumbnails #5812
Include Grease Pencil in review and thumbnails.
___
Workfiles tool AYON: Fix double click of workfile #5813
Fix double click on workfiles in workfiles tool to open the file.
___
Webpublisher: removal of usage of no_of_frames in error message #5819
If it throws exception, `no_of_frames` value wont be available, so it doesn't make sense to log it.
___
Attribute Defs: Hide multivalue widget in Number by default #5821
Fixed default look of `NumberAttrWidget` by hiding its multiselection widget.
___
Corrected a typo in Readme.md (Top -> To) #5800
___
Photoshop: Removed redundant copy of extension.zxp #5802
`extension.zxp` shouldn't be inside of extension folder.
___
Maya: Multi-shot Layout Creator #5710
New Multi-shot Layout creator is a way of automating creation of the new Layout instances in Maya, associated with correct shots, frame ranges and Camera Sequencer in Maya.
___
Colorspace: ociolook file product type workflow #5541
Traypublisher support for publishing of colorspace look files (ociolook) which are json files holding any LUT files. This new product is available for loading in Nuke host at the moment.Added colorspace selector to publisher attribute with better labeling. We are supporting also Roles and Alias (only v2 configs).
___
Scene Inventory tool: Refactor Scene Inventory tool (for AYON) #5758
Modified scene inventory tool for AYON. The main difference is in how project name is defined and replacement of assets combobox with folders dialog.
___
AYON: Support dev bundles #5783
Modules can be loaded in AYON dev mode from different location.
___
Testing: Ingest Maya userSetup #5734
Suggesting to ingest `userSetup.py` startup script for easier collaboration and transparency of testing.
___
Fusion: Work with pathmaps #5329
Path maps are a big part of our Fusion workflow. We map the project folder to a path map within Fusion so all loaders and savers point to the path map variable. This way any computer on any OS can open any comp no matter where the project folder is located.
___
Maya: Add Maya 2024 and remove pre 2022. #5674
Adding Maya 2024 as default application variant.Removing Maya 2020 and older, as these are not supported anymore.
___
Enhancement: Houdini: Allow using template keys in Houdini shelves manager #5727
Allow using Template keys in Houdini shelves manager.
___
Houdini: Fix Show in usdview loader action #5737
Fix the "Show in USD View" loader to show up in Houdini
___
Nuke: validator of asset context with repair actions #5749
Instance nodes with different context of asset and task can be now validated and repaired via repair action.
___
AYON: Tools enhancements #5753
Few enhancements and tweaks of AYON related tools.
___
Max: Tweaks on ValidateMaxContents #5759
This PR provides enhancements on ValidateMaxContent as follow:
- Rename `ValidateMaxContents` to `ValidateContainers`
- Add related families which are required to pass the validation(All families except `Render` as the render instance is the one which only allows empty container)
___
Enhancement: Nuke refactor `SelectInvalidAction` #5762
Refactor `SelectInvalidAction` to behave like other action for other host, create `SelectInstanceNodeAction` as dedicated action to select the instance node for a failed plugin.
- Note: Selecting Instance Node will still select the instance node even if the user has currently 'fixed' the problem.
___
Enhancement: Tweak logging for Nuke for artist facing reports #5763
Tweak logs that are not artist-facing to debug level + in some cases clarify what the logged value is.
___
AYON Settings: Disk mapping #5786
Added disk mapping settings to core addon settings.
___
Maya: add colorspace argument to redshiftTextureProcessor #5645
In color managed Maya, texture processing during Look Extraction wasn't passing texture colorspaces set on textures to `redshiftTextureProcessor` tool. This in effect caused this tool to produce non-zero exit code (even though the texture was converted into wrong colorspace) and therefor crash of the extractor. This PR is passing colorspace to that tool if color management is enabled.
___
Maya: don't call `cmds.ogs()` in headless mode #5769
`cmds.ogs()` is a call that will crash if Maya is running in headless mode (mayabatch, mayapy). This is handling that case.
___
Resolve: inventory management fix #5673
Loaded Timeline item containers are now updating correctly and version management is working as it suppose to.
- [x] updating loaded timeline items
- [x] Removing of loaded timeline items
___
Blender: Remove 'update_hierarchy' #5756
Remove `update_hierarchy` function which is causing crashes in scene inventory tool.
___
Max: bug fix on the settings in pointcloud family #5768
Bug fix on the settings being errored out in validate point cloud(see links:https://github.com/ynput/OpenPype/pull/5759#pullrequestreview-1676681705) and passibly in point cloud extractor.
___
AYON settings: Fix default factory of tools #5773
Fix default factory of application tools.
___
Fusion: added missing OPENPYPE_VERSION #5776
Fusion submission to Deadline was missing OPENPYPE_VERSION env var when submitting from build (not source code directly). This missing env var might break rendering on DL if path to OP executable (openpype_console.exe) is not set explicitly and might cause an issue when different versions of OP are deployed.This PR adds this environment variable.
___
Ftrack: Skip tasks when looking for asset equivalent entity #5777
Skip tasks when looking for asset equivalent entity.
___
Nuke: loading gizmos fixes #5779
Gizmo product is not offered in Loader as plugin. It is also updating as expected.
___
General: thumbnail extractor as last extractor #5780
Fixing issue with the order of the `ExtractOIIOTranscode` and `ExtractThumbnail` plugins. The problem was that the `ExtractThumbnail` plugin was processed before the `ExtractOIIOTranscode` plugin. As a result, the `ExtractThumbnail` plugin did not inherit the `review` tag into the representation data. This caused the `ExtractThumbnail` plugin to fail in processing and creating thumbnails.
___
Bug: fix key in application json #5787
In PR #5705 `maya` was wrongly used instead of `mayapy`, breaking AYON defaults in AYON Application Addon.
___
'NumberAttrWidget' shows 'Multiselection' label on multiselection #5792
Attribute definition widget 'NumberAttrWidget' shows `< Multiselection >` label on multiselection.
___
Publisher: Selection change by enabled checkbox on instance update attributes #5793
Change of instance by clicking on enabled checkbox will actually update attributes on right side to match the selection.
___
Houdini: Remove `setParms` call since it's responsibility of `self.imprint` to set the values #5796
Revert a recent change made in #5621 due to this comment. However the change is faulty as can be seen mentioned here
___
AYON loader: Fix SubsetLoader functionality #5799
Fix SubsetLoader plugin processing in AYON loader tool.
___
Houdini: Add self publish button #5621
This PR allows single publishing by adding a publish button to created rop nodes in HoudiniAdmins are much welcomed to enable it from houdini general settingsPublish Button also includes all input publish instances. in this screen shot the alembic instance is ignored because the switch is turned off
___
Nuke: fixing UNC support for OCIO path #5771
UNC paths were broken on windows for custom OCIO path and this is solving the issue with removed double slash at start of path
___
Maya: Add MayaPy application. #5705
This adds mayapy to the application to be launched from a task.
___
Feature: Copy resources when downloading last workfile #4944
When the last published workfile is downloaded as a prelaunch hook, all resource files referenced in the workfile representation are copied to the `resources` folder, which is inside the local workfile folder.
___
Blender: Deadline support #5438
Add Deadline support for Blender.
___
Fusion: implement toggle to use Deadline plugin FusionCmd #5678
Fusion 17 doesn't work in DL 10.3, but FusionCmd does. It might be probably better option as headless variant.Fusion plugin seems to be closing and reopening application when worker is running on artist machine, not so with FusionCmdAdded configuration to Project Settings for admin to select appropriate Deadline plugin:
___
Loader tool: Refactor loader tool (for AYON) #5729
Refactored loader tool to new tool. Separated backend and frontend logic. Refactored logic is AYON-centric and is used only in AYON mode, so it does not affect OpenPype. The tool is also replacing library loader.
___
Maya: implement matchmove publishing #5445
Add possibility to export multiple cameras in single `matchmove` family instance, both in `abc` and `ma`.Exposed flag 'Keep image planes' to control export of image planes.
___
Maya: Add optional Fbx extractors in Rig and Animation family #5589
This PR allows user to export control rigs(optionally with mesh) and animated rig in fbx optionally by attaching the rig objects to the two newly introduced sets.
___
Maya: Optional Resolution Validator for Render #5693
Adding optional resolution validator for maya in render family, similar to the one in Max.It checks if the resolution in render setting aligns with that in setting from the db.
___
Use host's node uniqueness for instance id in new publisher #5490
Instead of writing `instance_id` as parm or attributes on the publish instances we can, for some hosts, just rely on a unique name or path within the scene to refer to that particular instance. By doing so we fix #4820 because upon duplicating such a publish instance using the host's (DCC) functionality the uniqueness for the duplicate is then already ensured instead of attributes remaining exact same value as where to were duplicated from, making `instance_id` a non-unique value.
___
Max: Implementation of OCIO configuration #5499
Resolve #5473 Implementation of OCIO configuration for Max 2024 regarding to the update of Max 2024
___
Nuke: Multiple format supports for ExtractReviewDataMov #5623
This PR would fix the bug of the plugin `ExtractReviewDataMov` not being able to support extensions other than `mov`. The plugin is also renamed to `ExtractReviewDataBakingStreams` as i provides multiple format supoort.
___
Bugfix: houdini switching context doesnt update variables #5651
Allows admins to have a list of vars (e.g. JOB) with (dynamic) values that will be updated on context changes, e.g. when switching to another asset or task.Using template keys is supported but formatting keys capitalization variants is not, e.g. {Asset} and {ASSET} won't workDisabling Update Houdini vars on context change feature will leave all Houdini vars unmanaged and thus no context update changes will occur.Also, this PR adds a new button in menu to update vars on demand.
___
Publisher: Fix report maker memory leak + optimize lookups using set #5667
Fixes a memory leak where resetting publisher does not clear the stored plugins for the Publish Report Maker.Also changes the stored plugins to a `set` to optimize the lookup speeds.
___
Add openpype_mongo command flag for testing. #5676
Instead of changing the environment, this command flag allows for changing the database.
___
Nuke: minor docstring and code tweaks for ExtractReviewMov #5695
Code and docstring tweaks on https://github.com/ynput/OpenPype/pull/5623
___
AYON: Small settings fixes #5699
Small changes/fixes related to AYON settings. All foundry apps variant `13-0` has label `13.0`. Key `"ExtractReviewIntermediates"` is not mandatory in settings.
___
Blender: Alembic Animation loader #5711
Implemented loading Alembic Animations in Blender.
___
Maya: Missing "data" field and enabling of audio #5618
When updating audio containers, the field "data" was missing and the audio node was not enabled on the timeline.
___
Maya: Bug in validate Plug-in Path Attribute #5687
Overwriting list with string is causing `TypeError: string indices must be integers` in subsequent iterations, crashing the validator plugin.
___
General: Avoid fallback if value is 0 for handle start/end #5652
There's a bug on the `pyblish_functions.get_time_data_from_instance_or_context` where if `handleStart` or `handleEnd` on the instance are set to value 0 it's falling back to grabbing the handles from the instance context. Instead, the logic should be that it only falls back to the `instance.context` if the key doesn't exist.This change was only affecting me on the `handleStart`/`handleEnd` and it's unlikely it could cause issues on `frameStart`, `frameEnd` or `fps` but regardless, the `get` logic is wrong.
___
Fusion: added missing env vars to Deadline submission #5659
Environment variables discerning type of job was missing. Without this injection of environment variables won't start.
___
Nuke: workfile version synchronization settings fixed #5662
Settings for synchronizing workfile version to published products is fixed.
___
AYON Workfiles Tool: Open workfile changes context #5671
Change context when workfile is opened.
___
Blender: Fix remove/update in new layout instance #5679
Fixes an error that occurs when removing or updating an asset in a new layout instance.
___
AYON Launcher tool: Fix refresh btn #5685
Refresh button does propagate refreshed content properly. Folders and tasks are cached for 60 seconds instead of 10 seconds. Auto-refresh in launcher will refresh only actions and related data which is project and project settings.
___
Deadline: handle all valid paths in RenderExecutable #5694
This commit enhances the path resolution mechanism in the RenderExecutable function of the Ayon plugin. Previously, the function only considered paths starting with a tilde (~), ignoring other valid paths listed in exe_list. This limitation led to an empty expanded_paths list when none of the paths in exe_list started with a tilde, causing the function to fail in finding the Ayon executable.With this fix, the RenderExecutable function now correctly processes and includes all valid paths from exe_list, improving its reliability and preventing unnecessary errors related to Ayon executable location.
___
AYON Launcher tool: Fix skip last workfile boolean #5700
Skip last workfile boolean works as expected.
___
Chore: Explore here action can work without task #5703
Explore here action does not crash when task is not selected, and change error message a little.
___
Testing: Inject mongo_url argument earlier #5706
Fix for https://github.com/ynput/OpenPype/pull/5676The Mongo url is used earlier in the execution.
___
Blender: Add support to auto-install PySide2 in blender 4 #5723
Change version regex to support blender 4 subfolder.
___
Fix: Hardcoded main site and wrongly copied workfile #5733
Fixing these two issues:
- Hardcoded main site -> Replaced by `anatomy.fill_root`.
- Workfiles can sometimes be copied while they shouldn't.
___
Bugfix: ServerDeleteOperation asset -> folder conversion typo #5735
Fix ServerDeleteOperation asset -> folder conversion typo
___
Nuke: loaders are filtering correctly #5739
Variable name for filtering by extensions were not correct - it suppose to be plural. It is fixed now and filtering is working as suppose to.
___
Nuke: failing multiple thumbnails integration #5741
This handles the situation when `ExtractReviewIntermediates` (previously `ExtractReviewDataMov`) has multiple outputs, including thumbnails that need to be integrated. Previously, integrating the thumbnail representation was causing an issue in the integration process. However, we have now resolved this issue by no longer integrating thumbnails as loadable representations.NOW default is that thumbnail representation are NOT integrated (eg. they will not show up in DB > couldn't be Loaded in Loader) and no `_thumb.jpg` will be left in `render` (most likely) publish folder.IF there would be need to override this behavior, please use `project_settings/global/publish/PreIntegrateThumbnails`
___
AYON Settings: Fix global overrides #5745
The `output` dictionary that gets passed into `ayon_settings._convert_global_project_settings` gets replaced when converting the settings for `ExtractOIIOTranscode`. This results in `global` not being in the output dictionary and thus the defaults being used and not the project overrides.
___
Chore: AYON query functions arguments #5752
Fixed how `archived` argument is handled in get subsets/assets function.
___
Publisher: Refactor Report Maker plugin data storage to be a dict by plugin.id #5668
Refactor Report Maker plugin data storage to be a dict by `plugin.id`Also fixes `_current_plugin_data` type on `__init__`
___
Chore: Refactor Resolve into new style HostBase, IWorkfileHost, ILoadHost #5701
Refactor Resolve into new style HostBase, IWorkfileHost, ILoadHost
___
Chore: Maya reduce get project settings calls #5669
Re-use system settings / project settings where we can instead of requerying.
___
Extended error message when getting subset name #5649
Each Creator is using `get_subset_name` functions which collects context data and fills configured template with placeholders.If any key is missing in the template, non descriptive error is thrown.This should provide more verbose message:
___
Tests: Remove checks for env var #5696
Env var will be filled in `env_var` fixture, here it is too early to check
___
Unreal: Yeti support #5643
Implemented Yeti support for Unreal.
___
Houdini: Add Static Mesh product-type (family) #5481
This PR adds support to publish Unreal Static Mesh in Houdini as FBXQuick recap
- [x] Add UE Static Mesh Creator
- [x] Dynamic subset name like in Maya
- [x] Collect Static Mesh Type
- [x] Update collect output node
- [x] Validate FBX output node
- [x] Validate mesh is static
- [x] Validate Unreal Static Mesh Name
- [x] Validate Subset Name
- [x] FBX Extractor
- [x] FBX Loader
- [x] Update OP Settings
- [x] Update AYON Settings
___
Launcher tool: Refactor launcher tool (for AYON) #5612
Refactored launcher tool to new tool. Separated backend and frontend logic. Refactored logic is AYON-centric and is used only in AYON mode, so it does not affect OpenPype.
___
Maya: Use custom staging dir function for Maya renders - OP-5265 #5186
Check for custom staging dir when setting the renders output folder in Maya.
___
Colorspace: updating file path detection methods #5273
Support for OCIO v2 file rules integrated into the available color management API
___
Chore: add default isort config #5572
Add default configuration for isort tool
___
Deadline: set PATH environment in deadline jobs by GlobalJobPreLoad #5622
This PR makes `GlobalJobPreLoad` to set `PATH` environment in deadline jobs so that we don't have to use the full executable path for deadline to launch the dcc app. This trick should save us adding logic to pass houdini patch version and modifying Houdini deadline plugin. This trick should work with other DCCs
___
nuke: extract review data mov read node with expression #5635
Some productions might have set default values for read nodes, those settings are not colliding anymore now.
___
Maya: Support new publisher for colorsets validation. #5630
Fix `validate_color_sets` for the new publisher.In current `develop` the repair option does not appear due to wrong error raising.
___
Houdini: Camera Loader fix mismatch for Maya cameras #5584
This PR adds
- A workaround to match Maya render mask in Houdini
- `SetCameraResolution` inventory action
- set camera resolution when loading or updating camera
___
Nuke: fix set colorspace on writes #5634
Colorspace is set correctly to any write node created from publisher.
___
TVPaint: Fix review family extraction #5637
Extractor marks representation of review instance with review tag.
___
AYON settings: Extract OIIO transcode settings #5639
Output definitions of Extract OIIO transcode have name to match OpenPype settings, and the settings are converted to dictionary in settings conversion.
___
AYON: Fix task type short name conversion #5641
Convert AYON task type short name for OpenPype correctly.
___
colorspace: missing `allowed_exts` fix #5646
Colorspace module is not failing due to missing `allowed_exts` attribute.
___
Photoshop: remove trailing underscore in subset name #5647
If {layer} placeholder is at the end of subset name template and not used (for example in `auto_image` where separating it by layer doesn't make any sense) trailing '_' was kept. This updates cleaning logic and extracts it as it might be similar in regular `image` instance.
___
traypublisher: missing `assetEntity` in context data #5648
Issue with missing `assetEnity` key in context data is not problem anymore.
___
AYON: Workfiles tool save button works #5653
Fix save as button in workfiles tool.(It is mystery why this stopped to work??)
___
Max: bug fix delete items from container #5658
Fix the bug shown when clicking "Delete Items from Container" and selecting nothing and press ok.
___
Chore: Remove unused functions from Fusion integration #5617
Cleanup unused code from Fusion integration
___
Increase timout for deadline test #5654
DL picks up jobs quite slow, so bump up delay.
___
Chore: Remove schema from OpenPype root #5355
Remove unused schema directory in root of repository which was moved inside openpype/pipeline/schema.
___
Igniter: Allow custom Qt scale factor rounding policy #5554
Do not force `PassThrough` rounding policy if different policy is defined via env variable.
___
Chore: Lower urllib3 to support older OpenSSL #5538
Lowered `urllib3` to `1.26.16` to support older OpenSSL.
___
Chore: Do not try to add schema to zip files #5557
Do not add `schema` folder to zip file. This fixes issue cause by https://github.com/ynput/OpenPype/pull/5355 .
___
Chore: Lower click dependency version #5629
Lower click version to support older versions of python.
___
Bump certifi from 2023.5.7 to 2023.7.22 #5351
Bumps [certifi](https://github.com/certifi/python-certifi) from 2023.5.7 to 2023.7.22.
Commits
[](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)
You can trigger a rebase of this PR by commenting `@dependabot rebase`.
[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)
---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually
- `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
You can disable automated security fix PRs for this repo from the [Security Alerts page](https://github.com/ynput/OpenPype/network/alerts).
Maya: Extract active view as thumbnail when no thumbnail set #5426
This sets the Maya instance's thumbnail to the current active view if no thumbnail was set yet.
___
Maya: Implement USD publish and load using native `mayaUsdPlugin` #5573
Implement Creator and Loaders for extraction and loading of USD files using Maya's own `mayaUsdPlugin`.Also adds support to load a `usd` file into an Arnold Standin (`aiStandin`) and assigning looks to it.
___
AYON: Ignore separated modules #5619
Do not load already separated modules from default directory.
___
Maya: Reduce amount of code for Collect Looks #5253
- Refactor `get_file_node_files` because popping from `paths` by index should have been done in reversed order anyway. It's now changed to not need popping at all.
- Removed unused `RENDERER_NODE_TYPES` and if-branch which collected `node_attrs` list which was unused + collected members which was also done outside of the if branch and thus generated no extra data.
- Collected all materials from look set attributes at once instead of multiple queries
- Collected all file nodes in history from a single query instead of per type
- Restructured assignment of `instance.data["resources"]` to be more readable
- Cached `PXR_NODES` only ones (Note: plugin load is checked on discovery of the collect look plugin) instead of querying plugin load and its nodes per file node per attribute
- Removed some debug logs or combined some messages
___
AYON: Mark deprecated settings in Maya #5627
Added deprecated info to docstrings of maya colormanagement settings.Resolves: https://github.com/ynput/OpenPype/issues/5556
___
Max: switching versions of maxScene maintain parentage/links with the loaders #5424
When using scene inventory to manage or update the version of the loading objects, the linked modifiers or parentage of the objects would be kept.Meanwhile, loaded objects from all loaders no longer parented to the container with OP Data.
___
3ds max: small tweaks to obj extractor and model publishing flow #5605
There migh be situation where OBJ Extractor passes without failure, but no obj file is produced. This is adding simple check directly into the extractor to catch it earlier then in the integration phase. Also switched `Validate USD Plugin` to optional, because it was always run no matter if the Extract USD was enabled or not, hindering testing (and publishing).
___
TVPaint: Plugin can be reopened #5610
TVPaint plugin can be reopened.
___
Maya: Remove context prompt #5632
More of a plea than a PR, but could we please remove the context prompt in Maya when switching tasks?
___
General: Create a desktop icon is checked #5636
In OP Installer `Create a desktop icon` is checked by default.
___
Maya: Extract look is not AYON compatible - OP-5375 #5341
The textures that would use hardlinking are going through texture processors. Currently all texture processors are hardcoded to copy texture instead of respecting the settings of forcing to copy.The texture processors were last modified 4 months ago, so effectively all clients that are on any pipeline updated in the last 4 months wont be utilizing hardlinking at all, since the hardcoded texture processors will copy texture no matter the OS.This opts for completely disabling the hardlinking feature, while we figure out what to do about it.
___
Maya: Multiverse USD Override inherit from correct new style creator #5566
Fix Creator for Multiverse USD Override by inheriting from correct new style creator class type
___
Max: Bug Fix Alembic Loaders with Ornatrix #5434
Bugfix the alembic loader with both ornatrix alembic and max alembic supportsAdd the ornatrix alembic loaders for loading the alembic with Ornatrix-related modifiers.
___
AYON: Avoid creation of duplicated links #5593
Handle cases when an existing link should be recreated and do not create the same link multitple times during single publishing.
___
Extract Review: Multilayer specification for ffmpeg #5613
Extract review is specifying layer name when exr is multilayer.
___
Fussion: added support for Fusion 17 #5614
Fusion 17 still uses Python 3.6 which causes issues with some our delivered libraries. Vendorized necessary set for Python 3.6
___
Publisher: Fix screenshot widget #5615
Use correct super method name.EDITED:Removed fade animation which is not triggered at some cases, e.g. in Nuke the animation does not start. I do expect that is caused by `exec_` on the dialog, which blocks event processing to the animation, even when I've added the window as parent it still didn't trigger registered callback.Modified how the "empty" space is not filled by using paths instead of clear mode on painter. Added render hints to add antialiasing.
___
Photoshop: auto_images without alpha will not fail #5620
ExtractReview caused issue on `auto_image` instance without alpha channel, this fixes it.
___
Fix - _id key used instead of id in get_last_version_by_subset_name #5626
Just 'id' is not returned because value in fields. Caused KeyError.
___
Bugfix: create symlinks for ssl libs on Centos 7 #5633
Docker build was missing `libssl.1.1.so` and `libcrypto.1.1.so` symlinks needed by the executable itself, because Python is now explicitly built with OpenSSL 1.1.1
___
Documentation/local settings #5102
I completed the "Working with local settings" page. I updated the screenshot, wrote an explanation for each empty category, and if available, linked the more detailed pages already existing. I also added the "Environments" category.
___
Workfiles tool: Refactor workfiles tool (for AYON) #5550
Refactored workfiles tool to new tool. Separated backend and frontend logic. Refactored logic is AYON-centric and is used only in AYON mode, so it does not affect OpenPype.
___
AfterEffects: added validator for missing files in FootageItems #5590
Published composition in AE could contain multiple FootageItems as a layers. If FootageItem contains imported file and it doesn't exist, render triggered by Publish process will silently fail and no output is generated. This could cause failure later in the process with unclear reason. (In `ExtractReview`).This PR adds validation to protect from this.
___
Maya: Yeti Cache Include viewport preview settings from source #5561
When publishing and loading yeti caches persist the display output and preview colors + settings to ensure consistency in the view
___
Houdini: validate colorspace in review rop #5322
Adding a validator that checks if 'OCIO Colorspace' parameter on review rop was set to a valid value.It is a step towards managing colorspace in review ropvalid values are the ones in the dropdown menuthis validator also provides some helper actions This PR is related to #4836 and #4833
___
Colorspace: adding abstraction of publishing related functions #5497
The functionality of Colorspace has been abstracted for greater usability.
___
Nuke: removing redundant workfile colorspace attributes #5580
Nuke root workfile colorspace data type knobs are long time configured automatically via config roles or the default values are also working well. Therefore there is no need for pipeline managed knobs.
___
Ftrack: Less verbose logs for Ftrack integration in artist facing logs #5596
- Reduce artist-facing logs for component integration for Ftrack
- Avoid "Comment is not set" log in artist facing report for Kitsu and Ftrack
- Remove info log about `ffprobe` inspecting a file (changed to debug log)
- interesting to see however that it ffprobes the same jpeg twice - but maybe once for thumbnail?
___
Maya: Fix rig validators for new out_SET and controls_SET names #5595
Fix usage of `out_SET` and `controls_SET` since #5310 because they can now be prefixed by the subset name.
___
TrayPublisher: set default frame values to sequential data #5530
We are inheriting default frame handles and fps data either from project or setting them to 0. This is just for case a production will decide not to injest the sequential representations with asset based metadata.
___
Publisher: Screenshot opacity value fix #5576
Fix opacity value.
___
AfterEffects: fix imports of image sequences #5581
#4602 broke imports of image sequences.
___
AYON: Fix representation context conversion #5591
Do not fix `"folder"` key in representation context until it is needed.
___
ayon-nuke: default factory to lists #5594
Default factory were missing in settings schemas for complicated objects like lists and it was causing settings to be failing saving.
___
Maya: Fix look assigner showing no asset if 'not found' representations are present #5597
Fix Maya Look assigner failing to show any content if it finds an invalid container for which it can't find the asset in the current project. (This can happen when e.g. loading something from a library project).There was logic already to avoid this but there was a bug where it used variable `_id` which did not exist and likely had to be `asset_id`.I've fixed that and improved the logged message a bit, e.g.:
```
// Warning: openpype.hosts.maya.tools.mayalookassigner.commands : Id found on 22 nodes for which no asset is found database, skipping '641d78ec85c3c5b102e836b0'
```
Example not found representation in Loader:The issue isn't necessarily related to NOT FOUND representations but in essence boils down to finding nodes with asset ids that do not exist in the current project which could very well just be local meshes in your scene.**Note:**I've excluded logging the nodes themselves because that tends to be a very long list of nodes. Only downside to removing that is that it's unclear which nodes are related to that `id`. If there are any ideas on how to still provide a concise informational message about that that'd be great so I could add it. Things I had considered:
- Report the containers, issue here is that it's about asset ids on nodes which don't HAVE to be in containers - it could be local geometry
- Report the namespaces, issue here is that it could be nodes without namespaces (plus potentially not about ALL nodes in a namespace)
- Report the short names of the nodes; it's shorter and readable but still likely a lot of nodes.@tokejepsen @LiborBatek any other ideas?
___
Photoshop: fixed blank Flatten image #5600
Flatten image is simplified publishing approach where all visible layers are "flatten" and published together. This image could be used as a reference etc.This is implemented by auto creator which wasn't updated after first publish. This would result in missing newly created layers after `auto_image` instance was created.
___
Blender: Remove Hardcoded Subset Name for Reviews #5603
Fixes hardcoded subset name for Reviews in Blender.
___
TVPaint: Fix tool callbacks #5608
Do not wait for callback to finish.
___
Chore: Remove unused variables and cleanup #5588
Removing some unused variables. In some cases the unused variables _seemed like they should've been used - maybe?_ so please **double check the code whether it doesn't hint to an already existing bug**.Also tweaked some other small bugs in code + tweaked logging levels.
___
Chore: Loader log deprecation warning for 'fname' attribute #5587
Since https://github.com/ynput/OpenPype/pull/4602 the `fname` attribute on the `LoaderPlugin` should've been deprecated and set for removal over time. However, no deprecation warning was logged whatsoever and thus one usage appears to have sneaked in (fixed with this PR) and a new one tried to sneak in with a recent PR
___
Attribute Definitions: Multiselection enum def #5547
Added `multiselection` option to `EnumDef`.
___
Farm: adding target collector #5494
Enhancing farm publishing workflow.
___
Maya: Optimize validate plug-in path attributes #5522
- Optimize query (use `cmds.ls` once)
- Add Select Invalid action
- Improve validation report
- Avoid "Unknown object type" errors
___
Maya: Remove Validate Instance Attributes plug-in #5525
Remove Validate Instance Attributes plug-in.
___
Enhancement: Tweak logging for artist facing reports #5537
Tweak the logging of publishing for global, deadline, maya and a fusion plugin to have a cleaner artist-facing report.
- Fix context being reported correctly from CollectContext
- Fix ValidateMeshArnoldAttributes: fix when arnold is not loaded, fix applying settings, fix for when ai attributes do not exist
___
AYON: Update settings #5544
Updated settings in AYON addons and conversion of AYON settings in OpenPype.
___
Chore: Removed Ass export script #5560
Removed Arnold render script, which was obsolete and unused.
___
Nuke: Allow for knob values to be validated against multiple values. #5042
Knob values can now be validated against multiple values, so you can allow write nodes to be `exr` and `png`, or `16-bit` and `32-bit`.
___
Enhancement: Cosmetics for Higher version of publish already exists validation error #5190
Fix double spaces in message.Example output **after** the PR:
___
Nuke: publish existing frames on farm #5409
This PR proposes adding a fourth option in Nuke render publish called "Use Existing Frames - Farm". This would be useful when the farm is busy or when the artist lacks enough farm licenses. Additionally, some artists prefer rendering on the farm but still want to check frames before publishing.By adding the "Use Existing Frames - Farm" option, artists will have more flexibility and control over their render publishing process. This enhancement will streamline the workflow and improve efficiency for Nuke users.
___
Unreal: Create project in temp location and move to final when done #5476
Create Unreal project in local temporary folder and when done, move it to final destination.
___
TrayPublisher: adding audio product type into default presets #5489
Adding Audio product type into default presets so anybody can publish audio to their shots.
___
Global: avoiding cleanup of flagged representation #5502
Publishing folder can be flagged as persistent at representation level.
___
General: missing tag could raise error #5511
- avoiding potential situation where missing Tag key could raise error
___
Chore: Queued event system #5514
Implemented event system with more expected behavior of event system. If an event is triggered during other event callback, it is not processed immediately but waits until all callbacks of previous events are done. The event system also allows to not trigger events directly once `emit_event` is called which gives option to process events in custom loops.
___
Publisher: Tweak log message to provide plugin name after "Plugin" #5521
Fix logged message for settings automatically applied to plugin attributes
___
Houdini: Improve VDB Selection #5523
Improves VDB selection if selection is `SopNode`: return the selected sop nodeif selection is `ObjNode`: get the output node with the minimum 'outputidx' or the node with display flag
___
Maya: Refactor/tweak Validate Instance In same Context plug-in #5526
- Chore/Refactor: Re-use existing select invalid and repair actions
- Enhancement: provide more elaborate PublishValidationError report
- Bugfix: fix "optional" support by using `OptionalPyblishPluginMixin` base class.
___
Enhancement: Update houdini main menu #5527
This PR adds two updates:
- dynamic main menu
- dynamic asset name and task
___
Houdini: Reset FPS when clicking Set Frame Range #5528
_Similar to Maya,_ Make `Set Frame Range` resets FPS, issue https://github.com/ynput/OpenPype/issues/5516
___
Enhancement: Deadline plugins optimize, cleanup and fix optional support for validate deadline pools #5531
- Fix optional support of validate deadline pools
- Query deadline webservice only once per URL for verification, and once for available deadline pools instead of for every instance
- Use `deadlineUrl` in `instance.data` when validating pools if it is set.
- Code cleanup: Re-use existing `requests_get` implementation
___
Chore: PowerShell script for docker build #5535
Added PowerShell script to run docker build.
___
AYON: Deadline expand userpaths in executables list #5540
Expande `~` paths in executables list.
___
Chore: Use correct git url #5542
Fixed github url in README.md.
___
Chore: Create plugin does not expect system settings #5553
System settings are not passed to initialization of create plugin initialization (and `apply_settings`).
___
Chore: Allow custom Qt scale factor rounding policy #5555
Do not force `PassThrough` rounding policy if different policy is defined via env variable.
___
Houdini: Fix outdated containers pop-up on opening last workfile on launch #5567
Fix Houdini not showing outdated containers pop-up on scene open when launching with last workfile argument
___
Houdini: Improve errors e.g. raise PublishValidationError or cosmetics #5568
Improve errors e.g. raise PublishValidationError or cosmeticsThis also fixes the Increment Current File plug-in since due to an invalid import it was previously broken
___
Fusion: Code updates #5569
Update fusion code which contains obsolete code. Removed `switch_ui.py` script from fusion with related script in scripts.
___
Maya: Validate Shape Zero fix repair action + provide informational artist-facing report #5524
Refactor to PublishValidationError to allow the RepairAction to work + provide informational report message
___
Maya: Fix attribute definitions for `CreateYetiCache` #5574
Fix attribute definitions for `CreateYetiCache`
___
Max: Optional Renderable Camera Validator for Render Instance #5286
Optional validation to check on renderable camera being set up correctly for deadline submission.If not being set up correctly, it wont pass the validation and user can perform repair actions.
___
Max: Adding custom modifiers back to the loaded objects #5378
The custom parameters OpenpypeData doesn't show in the loaded container when it is being loaded through the loader.
___
Houdini: Use default_variant to Houdini Node TAB Creator #5421
Use the default variant of the creator plugins on the interactive creator from the TAB node search instead of hard-coding it to `Main`.
___
Nuke: adding inherited colorspace from instance #5454
Thumbnails are extracted with inherited colorspace collected from rendering write node.
___
Add kitsu credentials to deadline publish job #5455
This PR hopefully fixes this issue #5440
___
AYON: Fill entities during editorial #5475
Fill entities and update template data on instances during extract AYON hierarchy.
___
Ftrack: Fix version 0 when integrating to Ftrack - OP-6595 #5477
Fix publishing version 0 to Ftrack.
___
OCIO: windows unc path support in Nuke and Hiero #5479
Hiero and Nuke is not supporting windows unc path formatting in OCIO environment variable.
___
Deadline: Added super call to init #5480
DL 10.3 requires plugin inheriting from DeadlinePlugin to call super's **init** explicitly.
___
Nuke: fixing thumbnail and monitor out root attributes #5483
Nuke Root Colorspace settings for Thumbnail and Monitor Out schema was gradually changed between version 12, 13, 14 and we needed to address those changes individually for particular version.
___
Nuke: fixing missing `instance_id` error #5484
Workfiles with Instances created in old publisher workflow were rising error during converting method since they were missing `instance_id` key introduced in new publisher workflow.
___
Nuke: existing frames validator is repairing render target #5486
Nuke is now correctly repairing render target after the existing frames validator finds missing frames and repair action is used.
___
added UE to extract burnins families #5487
This PR fixes missing burnins in reviewables when rendering from UE.
___
Harmony: refresh code for current Deadline #5493
- Added support in Deadline Plug-in for new versions of Harmony, in particular version 21 and 22.
- Remove review=False flag on render instance
- Add farm=True flag on render instance
- Fix is_in_tests function call in Harmony Deadline submission plugin
- Force HarmonyOpenPype.py Deadline Python plug-in to py3
- Fix cosmetics/hound in HarmonyOpenPype.py Deadline Python plug-in
___
Publisher: Fix multiselection value #5505
Selection of multiple instances in Publisher does not cause that all instances change all publish attributes to the same value.
___
Publisher: Avoid warnings on thumbnails if source image also has alpha channel #5510
Avoids the following warning from `ExtractThumbnailFromSource`:
```
// pyblish.ExtractThumbnailFromSource : oiiotool WARNING: -o : Can't save 4 channels to jpeg... saving only R,G,B
```
___
Update ayon-python-api #5512
Update ayon python api and related callbacks.
___
Max: Fixing the bug of falling back to use workfile for Arnold or any renderers except Redshift #5520
Fix the bug of falling back to use workfile for Arnold
___
General: Fix Validate Publish Dir Validator #5534
Nonsensical "family" key was used instead of real value (as 'render' etc.) which would result in wrong translation of intermediate family names.Updated docstring.
___
have the addons loading respect a custom AYON_ADDONS_DIR #5539
When using a custom AYON_ADDONS_DIR environment variable that variable is used in the launcher correctly and downloads and extracts addons to there, however when running Ayon does not respect this environment variable
___
Deadline: files on representation cannot be single item list #5545
Further logic expects that single item files will be only 'string' not 'list' (eg. repre["files"] = "abc.exr" not repre["files"] = ["abc.exr"].This would cause an issue in ExtractReview later.This could happen if DL rendered single frame file with different frame value.
___
Webpublisher: better encode list values for click #5546
Targets could be a list, original implementation pushed it as a separate items, it must be added as `--targets webpulish --targets filepublish`.`wepublish_routes` handles triggering from UI, changes in `publish_functions` handle triggering from cmd (for tests, api access).
___
Houdini: Introduce imprint function for correct version in hda loader #5548
Resolve #5478
___
AYON: Fill entities during editorial (2) #5549
Fix changes made in https://github.com/ynput/OpenPype/pull/5475.
___
Max: OP Data updates in Loaders #5563
Fix the bug on the loaders not being able to load the objects when iterating key and values with the dict.Max prefers list over the list in dict.
___
Create Plugins: Better check of overriden '__init__' method #5571
Create plugins do not log warning messages about each create plugin because of wrong `__init__` method check.
___
Tests: fix unit tests #5533
Fixed failing tests.Updated Unreal's validator to match removed general one which had a couple of issues fixed.
___
Feature: Download last published workfile specify version #4998
Setting `workfile_version` key to hook's `self.launch_context.data` allow you to specify the workfile version you want sync service to download if none is matched locally. This is helpful if the last version hasn't been correctly published/synchronized, and you want to recover the previous one (or some you'd like).Version could be set in two ways:
- OP's absolute version, matching the `version` index in DB.
- Relative version in reverse order from the last one: `-2`, `-3`...I don't know where I should write documentation about that.
___
Maya: allow not creation of group for Import loaders #5427
This PR enhances previous one. All ReferenceLoaders could not wrap imported products into explicit group.Also `Import` Loaders have same options. Control for this is separate in Settings, eg. Reference might wrap loaded items in group, `Import` might not.
___
3dsMax: Settings for Ayon #5388
Max Addon Setting for Ayon
___
General: Navigation to Folder from Launcher #5404
Adds an action in launcher to open the directory of the asset.
___
Chore: Default variant in create plugin #5429
Attribute `default_variant` on create plugins always returns string and if default variant is not filled other ways how to get one are implemented.
___
Publisher: Thumbnail widget enhancements #5439
Thumbnails widget in Publisher has new 3 options to choose from: Paste (from clipboard), Take screenshot and Browse. Clear button and new options are not visible by default, user must expand options button to show them.
___
AYON: Update ayon api to '0.3.5' #5460
Updated ayon-python-api to 0.3.5.
___
AYON: Apply unknown ayon settings first #5435
Settings of custom addons are available in converted settings.
___
Maya: Fix wrong subset name of render family in deadline #5442
New Publisher is creating different subset names than previously which resulted in duplication of `render` string in final subset name of `render` family published on Deadline.This PR solves that, it also fixes issues with legacy instances from old publisher, it matches the subset name as was before.This solves same issue in Max implementation.
___
Maya: Fix setting of version to workfile instance #5452
If there are multiple instances of renderlayer published, previous logic resulted in unpredictable rewrite of instance family to 'workfile' if `Sync render version with workfile` was on.
___
Maya: Context plugin shouldn't be tied to family #5464
`Maya Current File` collector was tied to `workfile` unnecessary. It should run even if `workile` instance is not being published.
___
Unreal: Fix loading hero version for static and skeletal meshes #5393
Fixed a problem with loading hero versions for static ans skeletal meshes.
___
TVPaint: Fix 'repeat' behavior #5412
Calculation of frames for repeat behavior is working correctly.
___
AYON: Thumbnails cache and api prep #5437
Moved thumbnails cache from ayon python api to OpenPype and prepare AYON thumbnail resolver for new api functions. Current implementation should work with old and new ayon-python-api.
___
Nuke: Name of the Read Node should be updated correctly when switching versions or assets. #5444
Bug fixing of the read node's name not being updated correctly when setting version or switching asset.
___
Farm publishing: asymmetric handles fixed #5446
Handles are now set correctly on farm published product version if asymmetric were set to shot attributes.
___
Scene Inventory: Provider icons fix #5450
Fix how provider icons are accessed in scene inventory.
___
Fix typo on Deadline OP plugin name #5453
Surprised that no one has hit this bug yet... but it seems like there was a typo on the name of the OP Deadline plugin when submitting jobs to it.
___
AYON: Fix version attributes update #5472
Fixed updates of attribs in AYON mode.
___
Added missing defaults for import_loader #5447
___
Bug: Local settings don't open on 3.14.7 #5220
### Before posting a new ticket, have you looked through the documentation to find an answer?
Yes I have
### Have you looked through the existing tickets to find any related issues ?
Not yet
### Author of the bug
@FadyFS
### Version
3.15.11-nightly.3
### What platform you are running OpenPype on?
Linux / Centos
### Current Behavior:
the previous behavior (bug) :

### Expected Behavior:

### What type of bug is it ?
Happened only once in a particular configuration
### Which project / workfile / asset / ...
open settings with 3.14.7
### Steps To Reproduce:
1. Run openpype on the 3.15.11-nightly.3 version
2. Open settings in 3.14.7 version
### Relevant log output:
_No response_
### Additional context:
_No response_
___
Tests: Add automated targets for tests #5443
Without it plugins with 'automated' targets won't be triggered (eg `CloseAE` etc.)
___
AYON: 3rd party addon usage #5300
Prepare OpenPype code to be able use `ayon-third-party` addon which supply ffmpeg and OpenImageIO executables. Because they both can support to define custom arguments (more than one) a new functions were needed to supply.New functions are `get_ffmpeg_tool_args` and `get_oiio_tool_args`. They work similar to previous but instead of string are returning list of strings. All places using previous functions `get_ffmpeg_tool_path` and `get_oiio_tool_path` are now using new ones. They should be backwards compatible and even with addon if returns single argument.
___
AYON: Addon settings in OpenPype #5347
Moved settings addons to OpenPype server addon. Modified create package to create zip files for server for each settings addon and for openpype addon.
___
AYON: Add folder to template data #5417
Added `folder` to template data, so `{folder[name]}` can be used in templates.
___
Option to start versioning from 0 #5262
This PR adds a settings option to start all versioning from 0.This PR will replace #4455.
___
Ayon: deadline implementation #5321
Quick implementation of deadline in Ayon. New Ayon plugin added for Deadline repository
___
AYON: Remove AYON launch logic from OpenPype #5348
Removed AYON launch logic from OpenPype. The logic is outdated at this moment and is replaced by `ayon-launcher`.
___
Bug: Error on multiple instance rig with maya #5310
I change endswith method by startswith method because the set are automacaly name out_SET, out_SET1, out_SET2 ...
___
Applications: Use prelaunch hooks to extract environments #5387
Environment variable preparation is based on prelaunch hooks. This should allow to pass OCIO environment variables to farm jobs.
___
Applications: Launch hooks cleanup #5395
Use `set` instead of `list` for filtering attributes in launch hooks. Celaction hooks dir does not contain `__init__.py`. Celaction prelaunch hook is reusing `CELACTION_ROOT_DIR`. Launch hooks are using full import from `openpype.lib.applications`.
___
Applications: Environment variables order #5245
Changed order of set environment variables. First are set context environment variables and then project environment overrides. Also asset and task environemnt variables are optional.
___
Autosave preferences can be read after Nuke opens the script #5295
Looks like I need to open the script in Nuke to be able to correctly load the autosave preferences.This PR reads the Nuke script in context, and offers owerwriting the current script with autosaved one if autosave exists.
___
Resolve: Update with compatible resolve version and latest docs #5317
Missing information about compatible Resolve version and latest docs from https://github.com/ynput/OpenPype/tree/develop/openpype/hosts/resolve
___
Chore: Remove deprecated functions #5323
Removed functions/classes that are deprecated and marked to be removed.
___
Nuke Render and Prerender nodes Process Order - OP-3555 #5332
This PR exposes control over the order of processing of the instances, by sorting the instances created. The sorting happens on the `render_order` and subset name. If the knob `render_order` is found on the instance, we'll sort by that first before sorting by subset name.`render_order` instances are processed before nodes without `render_order`. This could be extended in the future by querying other knobs but I dont know of a usecase for this.Hardcoded the creator `order` attribute of the `prerender` class to be before the `render`. Could be exposed to the user/studio but dont know of a use case for this.
___
Unreal: Python Environment Improvements #5344
Automatically set `UE_PYTHONPATH` as `PYTHONPATH` when launching Unreal.
___
Unreal: Custom location for Unreal Ayon Plugin #5346
Added a new environment variable `AYON_BUILT_UNREAL_PLUGIN` to set an already existing and built Ayon Plugin for Unreal.
___
Unreal: Better handling of Exceptions in UE Worker threads #5349
Implemented a new `UEWorker` base class to handle exception during the execution of UE Workers.
___
Houdini: Add farm toggle on creation menu #5350
Deadline Farm publishing and Rendering for Houdini was possible with this PR #4825 farm publishing is enabled by default some ROP nodes which may surprise new users (like me).I think adding a toggle (on by default) on creation UI is better so that users will be aware that there's a farm option for this publish instance.ROPs Modified :
- [x] Mantra ROP
- [x] Karma ROP
- [x] Arnold ROP
- [x] Redshift ROP
- [x] Vray ROP
___
Ftrack: Sync to avalon settings #5353
Added roles settings for sync to avalon action.
___
Chore: Schemas inside OpenPype #5354
Moved/copied schemas from repository root inside openpype/pipeline.
___
AYON: Addons creation enhancements #5356
Enhanced AYON addons creation. Fix issue with `Pattern` typehint. Zip filenames contain version. OpenPype package is skipping modules that are already separated in AYON. Updated settings of addons.
___
AYON: Update staging icons #5372
Updated staging icons for staging mode.
___
Enhancement: Houdini Update pointcache labels #5373
To me it's logical to find pointcaches types listed one after another, but they were named differentlySo, I made this PR to update their labels
___
nuke: split write node product instance features #5389
Improving Write node product instances by allowing precise activation of specific features.
___
Max: Use the empty modifiers in container to store AYON Parameter #5396
Instead of adding AYON/OP Parameter along with other attributes inside the container, empty modifiers would be created to store AYON/OP custom attributes
___
AfterEffects: Removed unused imports #5397
Removed unused import from extract local render plugin file.
___
Nuke: adding BBox knob type to settings #5405
Nuke knob types in settings having new `Box` type for reposition nodes like Crop or Reformat.
___
SyncServer: Existence of module is optional #5413
Existence of SyncServer module is optional and not required. Added `sync_server` module back to ignored modules when openpype addon is created for AYON. Command `syncserver` is marked as deprecated and redirected to sync server cli.
___
Webpublisher: Self contain test publish logic #5414
Moved test logic of publishing to webpublisher. Simplified `remote_publish` to remove webpublisher specific logic.
___
Webpublisher: Cleanup targets #5418
Removed `remote` target from webpublisher and replaced it with 2 targets `webpublisher` and `automated`.
___
nuke: update server addon settings with box #5419
updtaing nuke ayon server settings for Box option in knob types.
___
Maya: fix validate frame range on review attached to other instances #5296
Fixes situation where frame range validator can't be turned off on models if they are attached to reviewable camera in Maya.
___
Maya: Apply project settings to creators #5303
Project settings were not applied to the creators.
___
Maya: Validate Model Content #5336
`assemblies` in `cmds.ls` does not seem to work;
```python
from maya import cmds
content_instance = ['|group2|pSphere1_GEO', '|group2|pSphere1_GEO|pSphere1_GEOShape', '|group1|pSphere1_GEO', '|group1|pSphere1_GEO|pSphere1_GEOShape']
assemblies = cmds.ls(content_instance, assemblies=True, long=True)
print(assemblies)
```
Fixing with string splitting instead.
___
Bugfix: Maya update defaults variable #5368
So, something was forgotten while moving out from `LegacyCreator` to `NewCreator``LegacyCreator` used `defaults` to list suggested subset names which was changed into `default_variants` in the the `NewCreator`and setting `defaults` to any values has no effect!This update affects:
- [x] Model
- [x] Set Dress
___
Chore: Python 2 support fix #5375
Fix Python 2 support by adding `click` into python 2 dependencies and removing f-string from maya.
___
Maya: do not create top level group on reference #5402
This PR allows to not wrapping loaded referenced assets in top level group either explicitly for artist or by configuration in Settings.Artists can control group creation in ReferenceLoader options.Default no group creation could be set by emptying `Group Name` in `project_settings/maya/load/reference_loader`
___
Settings: Houdini & Maya create plugin settings #5436
Fixes related to Maya and Houdini settings. Renamed `defaults` to `default_variants` in plugin settings to match attribute name on create plugin in both OpenPype and AYON settings. Fixed Houdini AYON settings where were missing settings for defautlt varaints and fixed Maya AYON settings where default factory had wrong assignment.
___
Maya: Hide CreateAnimation #5297
When converting `animation` family or loading a `rig` family, need to include the `animation` creator but hide it in creator context.
___
Nuke Anamorphic slate - Read pixel aspect from input #5304
When asset pixel aspect differs from rendered pixel aspect, Nuke slate pixel aspect is not longer taken from asset, but is readed via ffprobe.
___
Nuke - Allow ExtractReviewDataMov with no timecode knob #5305
ExtractReviewDataMov allows to specify file type. Trying to write some other extension than mov fails on generate_mov assuming that mov64_write_timecode knob exists.
___
Nuke: removing settings schema with defaults for OpenPype #5306
continuation of https://github.com/ynput/OpenPype/pull/5275
___
Bugfix: Dependency without 'inputLinks' not downloaded #5337
Remove condition that avoids downloading dependency without `inputLinks`.
___
Bugfix: Houdini Creator use selection even if it was toggled off #5359
When creating many product types (families) one after another without refreshing the creator window manually if you toggled `Use selection` once, all the later product types will use selection even if it was toggled offHere's Before it will keep use selection even if it was toggled off, unless you refresh window manuallyhttps://github.com/ynput/OpenPype/assets/20871534/8b890122-5b53-4c6b-897d-6a2f3aa3388aHere's After it works as expectedhttps://github.com/ynput/OpenPype/assets/20871534/6b1db990-de1b-428e-8828-04ab59a44e28
___
Houdini: Correct camera selection for karma renderer when using selected node #5360
When user creates the karma rop with selected camera by use selection, it will give the error message of "no render camera found in selection".This PR is to fix the bug of creating karma rop when using selected camera node in Houdini
___
AYON: Environment variables and functions #5361
Prepare code for ayon-launcher compatibility. Fix ayon launcher subprocess calls, added more checks for `AYON_SERVER_ENABLED`, use ayon launcher suitable environment variables in AYON mode and changed outputs of some functions. Replaced usages of `OPENPYPE_REPOS_ROOT` environment variable with `PACKAGE_DIR` variable -> correct paths are used.
___
Nuke: farm rendering of prerender ignore roots in nuke #5366
`prerender` family was using wrong subset, same as `render` which should be different.
___
Bugfix: Houdini update defaults variable #5367
So, something was forgotten while moving out from `LegacyCreator` to `NewCreator``LegacyCreator` used `defaults` to list suggested subset names which was changed into `default_variants` in the the `NewCreator`and setting `defaults` to any values has no effect!This update affects:
- [x] Arnold ASS
- [x] Arnold ROP
- [x] Karma ROP
- [x] Mantra ROP
- [x] Redshift ROP
- [x] VRay ROP
___
Publisher: Fix create/publish animation #5369
Use geometry movement instead of changing min/max width.
___
Unreal: Move unreal splash screen to unreal #5370
Moved splash screen code to unreal integration and removed import from Igniter.
___
Nuke: returned not cleaning of renders folder on the farm #5374
Previous PR enabled explicit cleanup of `renders` folder after farm publishing. This is not matching customer's workflows. Customer wants to have access to files in `renders` folder and potentially redo some frames for long frame sequences.This PR extends logic of marking rendered files for deletion only if instance doesn't have `stagingDir_persistent`.For backwards compatibility all Nuke instances have `stagingDir_persistent` set to True, eg. `renders` folder won't be cleaned after farm publish.
___
Nuke: loading sequences is working #5376
Loading image sequences was broken after the latest release, version 3.16. However, I am pleased to inform you that it is now functioning as expected.
___
AYON: Fix settings conversion for ayon addons #5377
AYON addon settings are available in system settings and does not have available the same values in `"modules"` subkey.
___
Nuke: OCIO env var workflow #5379
The OCIO environment variable needs to be consistently handled across all platforms. Nuke resolves the custom OCIO config path differently depending on the platform, so we included the ocio config path in the workfile with a partial replacement using an environment variable. Additionally, for Windows sessions, we replaced backward slashes with a TCL expression.
___
Unreal: Fix Unreal build script #5381
Define 'AYON_UNREAL_ROOT' environment variable in unreal addon.
___
3dsMax: Use relative path to MAX_HOST_DIR #5382
Use `MAX_HOST_DIR` to calculate startup script path instead of use relative path to `OPENPYPE_ROOT` environment variable.
___
Bugfix: Houdini abc validator error message #5386
When ABC path validator fails, it prints node objects not node paths or namesThis bug happened because of updating `get_invalid` method to return nodes instead of node pathsBeforeAfter
___
Nuke: node name influence product (subset) name #5392
Nuke now allows users to duplicate publishing instances, making the workflow easier. By duplicating a node and changing its name, users can set the product (subset) name in the publishing context.Users now have the ability to change the variant name in Publisher, which will automatically rename the associated instance node.
___
Houdini: delete redundant bgeo sop validator #5394
I found out that this `Validate BGEO SOP Path` validator is redundant, it catches two cases that are already implemented in "Validate Output Node". "Validate Output Node" works with `bgeo` as well as `abc` because `"pointcache"` is listed in its families
___
Nuke: workfile is not reopening after change of context #5399
Nuke no longer reopens the latest workfile when the context is changed to a different task using the Workfile tool. The issue also affected the Script Clean (from Nuke File menu) and Close feature, but it has now been fixed.
___
Bugfix: houdini hard coded project settings #5400
I made this PR to solve the issue with hard-coded settings in houdini
___
AYON: 3dsMax settings #5401
Keep `adsk_3dsmax` group in applications settings.
___
Bugfix: update defaults to default_variants in maya and houdini OP DCC settings #5407
On moving out to new creator in Maya and Houdini updating settings was missed.
___
Applications: Attributes creation #5408
Applications addon does not cause infinite server restart loop.
___
Max: fix the bug of handling Object deletion in OP Parameter #5410
If the object is added to the OP parameter and user delete it in the scene thereafter, it will error out the container with OP attributes. This PR resolves the bug.This PR also fixes the bug of not adding the attribute into OP parameter correctly when the user enables "use selections" to link the object into the OP parameter.
___
Colorspace: including environments from launcher process #5411
Fixed bug in GitHub PR where the OCIO config template was not properly formatting environment variables from System Settings `general/environment`.
___
Nuke: workfile template fixes #5428
Some bunch of small bugs needed to be fixed
___
Houdini, Max: Fix missed function interface change #5430
This PR https://github.com/ynput/OpenPype/pull/5321/files from @kalisp missed updating the `add_render_job_env_var` in Houdini and Max as they are passing an extra arg:
```
TypeError: add_render_job_env_var() takes 1 positional argument but 2 were given
```
___
Scene Inventory: Fix issue with 'sync_server' #5431
Fix accesss to `sync_server` attribute in scene inventory.
___
Unpack project: Fix import issue #5433
Added `load_json_file`, `replace_project_documents` and `store_project_documents` to mongo init.
___
Chore: Versions post fixes #5441
Fixed issues caused by my fault. Filled right version value to anatomy data.
___
Tests: Copy file_handler as it will be removed by purging ayon code #5357
Ayon code will get purged in the future from this repo/addon, therefore all `ayon_common` will be gone. `file_handler` gets internalized to tests as it is not used anywhere else.
___
Fusion - Set selected tool to active #5327
When you run the action to select a node, this PR makes the node-flow show the selected node + you'll see the nodes controls in the inspector.
___
Maya: All base create plugins #5326
Prepared base classes for each creator type in Maya. Extended `MayaCreatorBase` to have default implementations of common logic with instances which is used in each type of plugin.
___
Windows: Support long paths on zip updates. #5265
Support long paths for version extract on Windows.Use case is when having long paths in for example an addon. You can install to the C drive but because the zip files are extracted in the local users folder, it'll add additional sub directories to the paths and quickly get too long paths for Windows to handle the zip updates.
___
Blender: Added setting to set resolution and start/end frames at startup #5338
This PR adds `set_resolution_startup`and `set_frames_startup` settings. They automatically set respectively the resolution and start/end frames and FPS in Blender when opening a file or creating a new one.
___
Blender: Support for ExtractBurnin #5339
This PR adds support for ExtractBurnin for Blender, when publishing a Review.
___
Blender: Extract Camera as Alembic #5343
Added support to extract Alembic Cameras in Blender.
___
Maya: Validate Instance In Context #5335
Missing new publisher error so the repair action shows up.
___
Settings: Fix default settings #5311
Fixed defautl settings for shotgrid. Renamed `FarmRootEnumEntity` to `DynamicEnumEntity` and removed doubled ABC metaclass definition (all settings entities have abstract metaclass).
___
Deadline: missing context argument #5312
Updated function arguments
___
Qt UI: Multiselection combobox PySide6 compatibility #5314
- The check states are replaced with the values for PySide6
- `QtCore.Qt.ItemIsUserTristate` is used instead of `QtCore.Qt.ItemIsTristate` to avoid crashes on PySide6
___
Docker: handle openssl 1.1.1 for centos 7 docker build #5319
Move to python 3.9 has added need to use openssl 1.1.x - but it is not by default available on centos 7 image. This is fixing it.
___
houdini: fix typo in redshift proxy #5320
I believe there's a typo in `create_redshift_proxy.py` ( extra ` ) in filename, and I made this PR to suggest a fix
___
Houdini: fix wrong creator identifier in pointCache workflow #5324
FIxing a bug in publishing alembics, were invalid creator identifier caused missing family association.
___
Fix colorspace compatibility check #5334
for some reason a user may have `PyOpenColorIO` installed to his machine, _in my case it came with renderman._it can trick the compatibility check as `import PyOpenColorIO` won't raise an error however it may be an old version _like my case_Beforecompatibility check was true and It used wrapper directly After Fix It will use wrapper via subprocess instead
___
Remove forgotten dev logging #5315
___
Royal Render: Maya and Nuke support #5191
Basic working implementation of Royal Render support in Maya.It expects New publisher implemented in Maya.
___
Blender: Blend File Family #4321
Implementation of the Blend File family analogue to the Maya Scene one.
___
Houdini: simple bgeo publishing #4588
Support for simple publishing of bgeo files.
This is adding basic support for bgeo publishing in Houdini. It will allow publishing bgeo in all supported formats (selectable in the creator options). If selected node has `output` on sop level, it will be used automatically as path in file node.
___
General: delivery action add renamed frame number in Loader #5024
Frame Offset options for delivery in Openpype loader
___
Enhancement/houdini add path action for abc validator #5237
Add a default path attribute Action.it's a helper action more than a repair action, which used to add a default single value.
___
Nuke: auto apply all settings after template build #5277
Adding auto run of Apply All Settings after template is builder is finishing its process. This will apply Frame-range, Image size, Colorspace found in context of a task shot.
___
Harmony:Removed loader settings for Harmony #5289
It shouldn't be configurable, it is internal logic. By adding additional extension it wouldn't start to work magically.
___
AYON: Make appdirs case sensitive #5298
Appdirs for AYON are case sensitive for linux and mac so we needed to change them to match ayon launcher. Changed 'ayon' to 'AYON' and 'ynput' to 'Ynput'.
___
Traypublisher: Fix plugin order #5299
Frame range collector for traypublisher was moved to traypublisher plugins and changed order to make sure `assetEntity` is filled in `instance.data`.
___
Deadline: removing OPENPYPE_VERSION from some host submitters #5302
Removing deprecated method of adding OPENPYPE_VERSION to job environment. It was leftover and other hosts have already been cleared.
___
AYON: Fix args for workfile conversion util #5308
Workfile update conversion util function have right expected arguments.
___
Maya: Refactor imports to `lib.get_reference_node` since the other function… #5258
Refactor imports to `lib.get_reference_node` since the other function is deprecated.
___
General: Reduce usage of legacy io #4723
Replace usages of `legacy_io` with getter methods or reuse already available information. Create plugins using CreateContext are using context from CreateContext object. Loaders are usign getter function from context tools. Publish plugin are using information instance.data or context.data. In some cases were pieces of code refactored a little e.g. fps getter in maya.
___
Documentation: API docs reborn - yet again #4419
## Feature
Add functional base for API Documentation using Sphinx and AutoAPI.
After unsuccessful #2512, #834 and #210 this is yet another try. But this time without ambition to solve the whole issue. This is making Shinx script to work and nothing else. Any changes and improvements in API docs should be made in subsequent PRs.
## How to use it
You can run:
```sh
cd .\docs
make.bat html
```
or
```sh
cd ./docs
make html
```
This will go over our code and generate **.rst** files in `/docs/source/autoapi` and from those it will generate full html documentation in `/docs/build/html`.
During the build you'll see tons of red errors that are pointing to our issues:
1) **Wrong imports**
Invalid import are usually wrong relative imports (too deep) or circular imports.
2) **Invalid doc-strings**
Doc-strings to be processed into documentation needs to follow some syntax - this can be checked by running
`pydocstyle` that is already included with OpenPype
3) **Invalid markdown/rst files**
md/rst files can be included inside rst files using `.. include::` directive. But they have to be properly formatted.
## Editing rst templates
Everything starts with `/docs/source/index.rst` - this file should be properly edited, Right now it just includes `readme.rst` that in turn include and parse main `README.md`. This is entrypoint to API documentation. All templates generated by AutoAPI are in `/docs/source/autoapi`. They should be eventually commited to repository and edited too.
## Steps for enhancing API documentation
1) Run `/docs/make.bat html`
2) Read the red errors/warnings - fix it in the code
3) Run `/docs/make.bat html` again until there are not red lines
4) Edit rst files and add some meaningfull content there
> **Note**
> This can (should) be merged as is without doc-string fixes in the code or changes in templates. All additional improvements on API documentation should be made in new PRs.
> **Warning**
> You need to add new dependencies to use it. Run `create_venv`.
Connected to #2490
___
Global: custom location for OP local versions #4673
This provides configurable location to unzip Openpype version zips. By default, it was hardcoded to artist's app data folder, which might be problematic/slow with roaming profiles.Location must be accessible by user running OP Tray with write permissions (so `Program Files` might be problematic)
___
AYON: Update settings conversion #4837
Updated conversion script of AYON settings to v3 settings. PR is related to changes in addons repository https://github.com/ynput/ayon-addons/pull/6 . Changed how the conversion happens -> conversion output does not start with openpype defaults but as empty dictionary.
___
AYON: Implement integrate links publish plugin #4842
Implemented entity links get/create functions. Added new integrator which replaces v3 integrator for links.
___
General: Version attributes integration #4991
Implemented unified integrate plugin to update version attributes after all integrations for AYON. The goal is to be able update attribute values in a unified way to a version when all addon integrators are done, so e.g. ftrack can add ftrack id to matching version in AYON server etc.The can be stored under `"versionAttributes"` key.
___
AYON: Staging versions can be used #4992
Added ability to use staging versions in AYON mode.
___
AYON: Preparation for products #5038
Prepare ayon settings conversion script for `product` settings conversion.
___
Loader: Hide inactive versions in UI #5101
Added support for `active` argument to hide versions with active set to False in Loader UI when in AYON mode.
___
General: CLI addon command #5109
Added `addon` alias for `module` in OpenPype cli commands.
___
AYON: OpenPype as server addon #5199
OpenPype repository can be converted to AYON addon for distribution. Addon has defined dependencies that are required to use it and are not in base ayon-launcher (desktop application).
___
General: Runtime dependencies #5206
Defined runtime dependencies in pyproject toml. Moved python ocio and otio modules there.
___
AYON: Bundle distribution #5209
Since AYON server 0.3.0 are addon versions defined by bundles which affects how addons, dependency packages and installers are handled. Only source of truth, about any version of anything that should be used, is server bundle.
___
Feature/blender handle q application #5264
This edit is to change the way the QApplication is run for Blender. It calls in the singleton (QApplication) during the register. This is made so that other Qt applications and addons are able to run on Blender. In its current implementation, if a QApplication is already running, all functionality of OpenPype becomes unavailable.
___
General: Connect to AYON server (base) #3924
Initial implementation of being able use AYON server in current OpenPype client. Added ability to connect to AYON server and use base queries.
AYON mode has it's own executable (and start script). To start in AYON mode just replace `start.py` with `ayon_start.py` (added tray start script to tools). Added constant `AYON_SERVER_ENABLED` to `openpype/__init__.py` to know if ayon mode is enabled. In that case Mongo is not used at all and any attempts will cause crashes.I had to modify `~/openpype/client` content to be able do this switch. Mongo implementation was moved to `mongo` subfolder and use "star imports" in files from where current imports are used. Logic of any tool or query in code was not changed at all. Since functions were based on mongo queries they don't use full potential of AYON server abilities.ATM implementation has login UI, distribution of files from server and replacement of mongo queries. For queries is used `ayon_api` module. Which is in live development so the versions may change from day to day.
___
Enhancement kitsu note with exceptions #4537
Adding a setting to choose some exceptions to IntegrateKitsuNote task status changes.
___
General: Environment variable for default OCIO configs #4670
Define environment variable which lead to root of builtin ocio configs to be able change the root without changing settings. For the path in settings was used `"{OPENPYPE_ROOT}/vendor/bin/ocioconfig/OpenColorIOConfig"` which disallow to change the root somewhere else. That will be needed in AYON where configs won't be part of desktop application but downloaded from server.
___
AYON: Editorial hierarchy creation #4699
Implemented extract hierarchy to AYON plugin which created entities in AYON using ayon api.
___
AYON: Vendorize ayon api #4753
Vendorize ayon api into openpype vendor directory. The reason is that `ayon-python-api` is in live development and will fix/add features often in next few weeks/months, and because update of dependency requires new release -> new build, we want to avoid the need of doing that as it would affect OpenPype development.
___
General: Update PySide 6 for MacOs #4764
New version of PySide6 does not have issues with settings UI. It is still breaking UI stylesheets so it is not changed for other plaforms but it is enhancement from previous state.
___
General: Removed unused cli commands #4902
Removed `texturecopy` and `launch` cli commands from cli commands.
___
AYON: Linux & MacOS launch script #4970
Added shell script to launch tray in AYON mode.
___
General: Qt scale enhancement #5059
Set ~~'QT_SCALE_FACTOR_ROUNDING_POLICY'~~ scale factor rounding policy of QApplication to `PassThrough` so the scaling can be 'float' number and not just 'int' (150% -> 1.5 scale).
___
CI: WPS linting instead of Hound (rebase) 2 #5115
Because Hound currently used to lint the code on GH ships with really old flake8 support, it fails miserably on any newer Python syntax. This PR is adding WPS linter to GitHub workflows that should step in.
___
Max: OP parameters only displays what is attached to the container #5229
The OP parameter in 3dsmax only displays what is currently attached to the container while deleting while you can see the items which is not added when you are adding to the container.
___
Testing: improving logging during testing #5271
Unit testing logging was crashing on more then one nested layers of inherited loggers.
___
Nuke: removing deprecated settings in baking #5275
Removing deprecated settings for baking with reformat. This option was only for single reformat node and it had been substituted with multiple reposition nodes.
___
AYON: General fixes and updates #4975
Few smaller fixes related to AYON connection. Some of fixes were taken from this PR.
___
Start script: Change returncode on validate or list versions #4515
Change exit code from `1` to `0` when versions are printed or when version is validated.
Return code `1` is indicating error but there didn't happen any error.
___
AYON: Change login UI works #4754
Fixed change of login UI. Logic change UI did show up, new login was successful, but after restart was used the previous login. This change fix the issue.
___
AYON: General issues #4763
Vendorized `ayon_api` from PR broke OpenPype launch, because `ayon_api` is not available. Moved `ayon_api` from ayon specific subforlder to `common` python vendor in OpenPype, and removed login in ayon start script (which was invalid anyway). Also made fixed compatibility with PySide6 by using `qtpy` instead of `Qt` and changing code which is not PySide6 compatible.
___
AYON: Small fixes #4841
Bugsfixes and enhancements related to AYON logic. Define `BUILTIN_OCIO_ROOT` environment variable so OCIO configs are working. Use constants from ayon api instead of hardcoding them in codebase. Change process name from "openpype" to "ayon". Don't execute login dialog when application is not yet running but use `open` method instead. Fixed missing modules settings which were not taken from openpype defaults. Updated ayon api to `0.1.17`.
___
Bugfix - Update gazu to 0.9.3 #4845
This updates Gazu to 0.9.3 to make sure Gazu works with Kitsu and Zou 0.16.x+
___
Igniter: fix error reports in silent mode #4909
Some errors in silent mode commands in Igniter were suppressed and not visible for example in Deadline log.
___
General: Remove ayon api from poetry lock #4964
Remove AYON python api from pyproject.toml and poetry.lock again.
___
Ftrack: Fix AYON settings conversion #4967
Fix conversion of ftrack settings in AYON mode.
___
AYON: ISO date format conversion issues #4981
Function `datetime.fromisoformat` was replaced with `arrow.get` to be used instead.
___
AYON: Missing files on representations #4989
Fix integration of files into representation in server database.
___
General: Fix Python 2 vendor for arrow #4993
Moved remaining dependencies for arrow from ftrack to python 2 vendor.
___
General: Fix new load plugins for next minor relase #5000
Fix access to `fname` attribute which is not available on load plugin anymore.
___
General: Fix mongo secure connection #5031
Fix `ssl` and `tls` keys checks in mongo uri query string.
___
AYON: Fix site sync settings #5069
Fixed settings for AYON variant of sync server.
___
General: Replace deprecated keyword argument in PyMongo #5080
Use argument `tlsCAFile` instead of `ssl_ca_certs` to avoid deprecation warnings.
___
Igniter: QApplication is created #5081
Function `_get_qt_app` actually creates new `QApplication` if was not created yet.
___
General: Lower unidecode version #5090
Use older version of Unidecode module to support Python 2.
___
General: Lower cryptography to 39.0.0 #5099
Lower cryptography to 39.0.0 to avoid breaking of DCCs like Maya and Nuke.
___
AYON: Global environments key fix #5118
Seems that when converting ayon settings to OP settings the `environments` setting is put under the `environments` key in `general` however when populating the environment the `environment` key gets picked up, which does not contain the environment variables from the `core/environments` setting
___
Add collector to tray publisher for getting frame range data #5152
Add collector to tray publisher to get frame range data. User can choose to enable this collector if they need this in the publisher.Resolve #5136
___
Unreal: get current project settings not using unreal project name #5170
There was a bug where Unreal project name was used to query project settings. But Unreal project name can differ from the "real" one because of naming convention rules set by Unreal. This is fixing it by asking for current project settings.
___
Substance Painter: Fix Collect Texture Set Images unable to copy.deepcopy due to QMenu #5238
Fix `copy.deepcopy` of `instance.data`.
___
Ayon: server returns different key #5251
Package returned from server has `filename` instead of `name`.
___
Substance Painter: Fix default color management settings #5259
The default settings for color management for Substance Painter were invalid, it was set to override the global config by default but specified no valid config paths of its own - and thus errored that the paths were not correct.This sets the defaults correctly to match other hosts._I quickly checked - this seems to be the only host with the wrong default settings_
___
Nuke: fixing container data if windows path in value #5267
Windows path in container data are reformatted. Previously it was reported that Nuke was rising `utf8 0xc0` error if backward slashes were in data values.
___
Houdini: fix typo error in collect arnold rop #5281
Fixing a typo error in `collect_arnold_rop.py`Reference: #5280
___
Slack - enhanced logging and protection against failure #5287
Covered issues found in production on customer site. SlackAPI exception doesn't need to have 'error', covered uncaught exception.
___
Maya: Removed unnecessary import of pyblish.cli #5292
This import resulted in adding additional logging handler which lead to duplication of logs in hosts with plugins containing `is_in_tests` method. Import is unnecessary for testing functionality.
___
Loader: Remove `context` argument from Loader.__init__() #4602
Remove the previously required `context` argument.
___
Global: Remove legacy integrator #4786
Remove the legacy integrator.
___
Next Minor Release #5291
___
Maya: Refactor to new publisher #4388
**Refactor Maya to use the new publisher with new creators.**
- [x] Legacy instance can be converted in UI using `SubsetConvertorPlugin`
- [x] Fix support for old style "render" and "vrayscene" instance to the new per layer format.
- [x] Context data is stored with scene
- [x] Workfile instance converted to AutoCreator
- [x] Converted Creator classes
- [x] Create animation
- [x] Create ass
- [x] Create assembly
- [x] Create camera
- [x] Create layout
- [x] Create look
- [x] Create mayascene
- [x] Create model
- [x] Create multiverse look
- [x] Create multiverse usd
- [x] Create multiverse usd comp
- [x] Create multiverse usd over
- [x] Create pointcache
- [x] Create proxy abc
- [x] Create redshift proxy
- [x] Create render
- [x] Create rendersetup
- [x] Create review
- [x] Create rig
- [x] Create setdress
- [x] Create unreal skeletalmesh
- [x] Create unreal staticmesh
- [x] Create vrayproxy
- [x] Create vrayscene
- [x] Create xgen
- [x] Create yeti cache
- [x] Create yeti rig
- [ ] Tested new Creator publishes
- [x] Publish animation
- [x] Publish ass
- [x] Publish assembly
- [x] Publish camera
- [x] Publish layout
- [x] Publish look
- [x] Publish mayascene
- [x] Publish model
- [ ] Publish multiverse look
- [ ] Publish multiverse usd
- [ ] Publish multiverse usd comp
- [ ] Publish multiverse usd over
- [x] Publish pointcache
- [x] Publish proxy abc
- [x] Publish redshift proxy
- [x] Publish render
- [x] Publish rendersetup
- [x] Publish review
- [x] Publish rig
- [x] Publish setdress
- [x] Publish unreal skeletalmesh
- [x] Publish unreal staticmesh
- [x] Publish vrayproxy
- [x] Publish vrayscene
- [x] Publish xgen
- [x] Publish yeti cache
- [x] Publish yeti rig
- [x] Publish workfile
- [x] Rig loader correctly generates a new style animation creator instance
- [ ] Validations / Error messages for common validation failures look nice and usable as a report.
- [ ] Make Create Animation hidden to the user (should not create manually?)
- [x] Correctly detect difference between **'creator_attributes'** and **'instance_data'** since both are "flattened" to the top node.
___
Start script: Fix possible issues with destination drive path #4478
Drive paths for windows are fixing possibly missing slash at the end of destination path.
Windows `subst` command require to have destination path with slash if it's a drive (it should be `G:\` not `G:`).
___
Global: Move PyOpenColorIO to vendor/python #4946
So that DCCs don't conflict with their own.
See https://github.com/ynput/OpenPype/pull/4267#issuecomment-1537153263 for the issue with Gaffer.
I'm not sure if this is the correct approach, but I assume PySide/Shiboken is under `vendor/python` for this reason as well...
___
RuntimeError with Click on deadline publish #5065
I changed Click to version 8.0 instead of 7.1.2 to solve this error:
```
2023-05-30 16:16:51: 0: STDOUT: Traceback (most recent call last):
2023-05-30 16:16:51: 0: STDOUT: File "start.py", line 1126, in boot
2023-05-30 16:16:51: 0: STDOUT: File "/prod/softprod/apps/openpype/LINUX/3.15/dependencies/click/core.py", line 829, in __call__
2023-05-30 16:16:51: 0: STDOUT: return self.main(*args, **kwargs)
2023-05-30 16:16:51: 0: STDOUT: File "/prod/softprod/apps/openpype/LINUX/3.15/dependencies/click/core.py", line 760, in main
2023-05-30 16:16:51: 0: STDOUT: _verify_python3_env()
2023-05-30 16:16:51: 0: STDOUT: File "/prod/softprod/apps/openpype/LINUX/3.15/dependencies/click/_unicodefun.py", line 126, in _verify_python3_env
2023-05-30 16:16:51: 0: STDOUT: raise RuntimeError(
2023-05-30 16:16:51: 0: STDOUT: RuntimeError: Click will abort further execution because Python 3 was configured to use ASCII as encoding for the environment. Consult https://click.palletsprojects.com/python3/ for mitigation steps.
```
___
Tray Publisher: User can set colorspace per instance explicitly #4901
With this feature a user can set/override the colorspace for the representations of an instance explicitly instead of relying on the File Rules from project settings or alike. This way you can ingest any file and explicitly say "this file is colorspace X".
___
Review Family in Max #5001
Review Feature by creating preview animation in 3dsmax(The code is still cleaning up so there is going to be some updates until it is ready for review)
___
AfterEffects: support for workfile template builder #5163
This PR add functionality of templated workfile builder. It allows someone to prepare AE workfile with placeholders as for automatically loading particular representation of particular subset of particular asset from context where workfile is opened.Selection from multiple prepared workfiles is provided with usage of templates, specific type of tasks could use particular workfile template etc.Artists then can build workfile from template when opening new workfile.
___
CreatePlugin: Get next version helper #5242
Implemented helper functions to get next available versions for create instances.
___
Maya: Improve Templates #4854
Use library method for fetching reference node and support parent in hierarchy.
___
Bug: Maya - xgen sidecar files arent moved when saving workfile as an new asset workfile changing context - OP-6222 #5215
This PR manages the Xgen files when switching context in the Workfiles app.
___
node references to check for duplicates in Max #5192
No duplicates for node references in Max when users trying to select nodes before publishing
___
Tweak profiles logging to debug level #5194
Tweak profiles logging to debug level since they aren't artist facing logs.
___
Enhancement: Reduce more visual clutter for artists in new publisher reports #5208
Got this from one of our artists' reports - figured some of these logs were definitely not for the artist, reduced those logs to debug level.
___
Cosmetics: Tweak pyblish repair actions (icon, logs, docstring) #5213
- Add icon to RepairContextAction
- logs to debug level
- also add attempt repair for RepairAction for consistency
- fix RepairContextAction docstring to mention correct argument name
#### Additional info
We should not forget to remove this ["deprecated" actions.py file](https://github.com/ynput/OpenPype/blob/3501d0d23a78fbaef106da2fffe946cb49bef855/openpype/action.py) in 3.16 (next-minor)
## Testing notes:
1. Run some fabulous repairs!
___
Maya: fix save file prompt on launch last workfile with color management enabled + restructure `set_colorspace` #5225
- Only set `configFilePath` when OCIO env var is not set since it doesn't do anything if OCIO var is set anyway.
- Set the Maya 2022+ default OCIO path using the resources path instead of "" to avoid Maya Save File on new file after launch
- **Bugfix: This is what fixes the Save prompt on open last workfile feature with Global color management enabled**
- Move all code related to applying the maya settings together after querying the settings
- Swap around the `if use_workfile_settings` since the check was reversed
- Use `get_current_project_name()` instead of environment vars
___
Enhancement: More descriptive error messages for Loaders #5227
Tweak raised errors and error messages for loader errors.
___
Houdini: add select invalid action for ValidateSopOutputNode #5231
This PR adds `SelectROPAction` action to `houdini\api\action.py`and it's used in `Validate Output Node``SelectROPAction` is used to select the associated ROPs with the errored instances.
___
Remove new lines from the delivery template string #5235
If the delivery template has a new line symbol at the end, say it was copied from the text editor, the delivery process will fail with `OSError` due to incorrect destination path. To avoid that I added `rstrip()` to the `delivery_path` processing.
___
Houdini: better selection on pointcache creation #5250
Houdini allows `ObjNode` path as `sop_path` in the `ROP` unlike OP/ Ayon require `sop_path` to be set to a sop node path explicitly In this code, better selection is used to filter out invalid selections from OP/ Ayon point of viewValid selections are
- `SopNode` that has parent of type `geo` or `subnet`
- `ObjNode` of type `geo` that has
- `SopNode` of type `output`
- `SopNode` with render flag `on` (if no `Sopnode` of type `output`)this effectively filter
- empty `ObjNode`
- `ObjNode`(s) of other types like `cam` and `dopnet`
- `SopNode`(s) that thier parents of other types like `cam` and `sop solver`
___
Update scene inventory even if any errors occurred during update #5252
When selecting many items in the scene inventory to update versions and one of the items would error out the updating stops. However, before this PR the scene inventory would also NOT refresh making you think it did nothing.Also implemented as method to allow some code deduplication.
___
Maya: Convert frame values to integers #5188
Convert frame values to integers.
___
Maya: fix the register_event_callback correctly collecting workfile save after #5214
fixing the bug of register_event_callback not being able to collect action of "workfile_save_after" for lock file action
___
Maya: aligning default settings to distributed aces 1.2 config #5233
Maya colorspace setttings defaults are set the way they align our distributed ACES 1.2 config file set in global colorspace configs.
___
RepairAction and SelectInvalidAction filter instances failed on the exact plugin #5240
RepairAction and SelectInvalidAction actually filter to instances that failed on the exact plugin - not on "any failure"
___
Maya: Bugfix look update nodes by id with non-unique shape names (query with `fullPath`) #5257
Fixes a bug where updating attributes on nodes with assigned shader if shape name existed more than once in the scene due to `cmds.listRelatives` call not being done with the `fullPath=True` flag.Original error:
```python
# Traceback (most recent call last):
# File "E:\openpype\OpenPype\openpype\tools\sceneinventory\view.py", line 264, in Nuke: Create nodes with inpanel=False #5051
This PR is meant to remove the annoyance of the UI changing focus to the properties window just for the property window of the newly created node to disappear. Instead of using node.hideControlPanel I'm implementing the concealment during the creation of the node which will not change the focus of the current window.
___
Fix the reset frame range not setting up the right timeline in Max #5187
Resolve #5181
___
Resolve: after launch automatization fixes #5193
Workfile is no correctly created and aligned witch actual project. Also the launching mechanism is now fixed so even no workfile had been saved yet it will open OpenPype menu automatically.
___
General: Revert backward incompatible change of path to template to multiplatform #5197
Now platformity is still handed by usage of `work[root]` (or any other root that is accessible across platforms.)
___
Nuke: root set format updating in node graph #5198
Nuke root node needs to be reset on some values so any knobs could be updated in node graph. This works the same way as an user would change frame number so expressions would update its values in knobs.
___
Hiero: fixing otio current project and cosmetics #5200
Otio were not returning correct current project once additional Untitled project was open in project manager stack.
___
Max: Publisher instances dont hold its enabled disabled states when Publisher reopened again #5202
Resolve #5183, general maxscript conversion issue to python (e.g. bool conversion, true in maxscript while True in Python)(Also resolve the ValueError when you change the subset to publish into list view menu)
___
Burnins: Filter script is defined only for video streams #5205
Burnins are working for inputs with audio.
___
Colorspace lib fix compatible python version comparison #5212
Fix python version comparison.
___
Houdini: Fix `get_color_management_preferences` #5217
Fix the issue described here where the logic for retrieving the current OCIO display and view was incorrectly trying to apply a regex to it.
___
Houdini: Redshift ROP image format bug #5218
Problem :
"RS_outputFileFormat" parm value was missing
and there were more "image_format" than redshift rop supports
Fix:
1) removed unnecessary formats from `image_format_enum`
2) add the selected format value to `RS_outputFileFormat`
___
Colorspace: check PyOpenColorIO rather then python version #5223
Fixing previously merged PR (https://github.com/ynput/OpenPype/pull/5212) And applying better way to check compatibility with PyOpenColorIO python api.
___
Validate delivery action representations status #5228
- disable delivery button if no representations checked
- fix macos combobox layout
- add error message if no delivery templates found
___
Houdini: Add geometry check for pointcache family #5230
When `sop_path` on ABC ROP node points to a non `SopNode`, these validators `validate_abc_primitive_to_detail.py`, `validate_primitive_hierarchy_paths.py` will error and crash when this line is executed `geo = output_node.geometryAtFrame(frame)`
___
Houdini: Add geometry check for VDB family #5232
When `sop_path` on Geometry ROP node points to a non SopNode, this validator `validate_vdb_output_node.py` will error and crash when this line is executed`sop_node.geometryAtFrame(frame)`
___
Substance Painter: Include the setting only in publish tab #5234
Instead of having two settings in both create and publish tab, there is solely one setting in the publish tab for users to set up the parameters.Resolve #5172
___
Maya: Fix collecting arnold prefix when none #5243
When no prefix is specified in render settings, the renderlayer collector would error.
___
Deadline: OPENPYPE_VERSION should only be added when running from build #5244
When running from source the environment variable `OPENPYPE_VERSION` should not be added. This is a bugfix for the feature #4489
___
Fix no prompt for "unsaved changes" showing when opening workfile in Houdini #5246
Fix no prompt for "unsaved changes" showing when opening workfile in Houdini.
___
Fix no prompt for "unsaved changes" showing when opening workfile in Substance Painter #5248
Fix no prompt for "unsaved changes" showing when opening workfile in Substance Painter.
___
General: add the os library before os.environ.get #5249
Adding os library into `creator_plugins.py` due to `os.environ.get` in line 667
___
Maya: Fix set_attribute for enum attributes #5261
Fix for #5260
___
Unreal: Move Qt imports away from module init #5268
Importing `Window` creates errors in headless mode.
```
*** WRN: >>> { ModulesLoader }: [ FAILED to import host folder unreal ]
=============================
No Qt bindings could be found
=============================
Traceback (most recent call last):
File "C:\Users\tokejepsen\OpenPype\.venv\lib\site-packages\qtpy\__init__.py", line 252, in Maya: Minor refactoring and code cleanup #5226
Some small cleanup and refactoring of logic. Removing old comments, unused imports and some minor optimization. Also removed the prints of the loader names of each container the scene in `fix_incompatible_containers` + optimizing by using `set` and defining only once. Moved some UI related code/tweaks to run `on_init` only if not in headless mode. Removed an empty `obj.py` file.Each commit message kind of describes why the change was made.
___
Bug: Template builder fails when loading data without outliner representation #5222
I add an assertion management in case the container does not have a represention in outliner.
___
AfterEffects - add container check validator to AE settings #5203
Adds check if scene contains only latest version of loaded containers.
___
Ftrack: Task status during publishing #5123
Added option to change task status during publishing for 3 possible cases: "sending to farm", "local integration" and "on farm integration".
___
Nuke: Allow for more complex temp rendering paths #5132
When changing the temporary rendering template (i.e., add `{asset}` to the path) to something a bit more complex the formatting was erroring due to missing keys.
___
Blender: Add support for custom path for app templates #5137
This PR adds support for a custom App Templates path in Blender by setting the `BLENDER_USER_SCRIPTS` environment variable to the path specified in `OPENPYPE_APP_TEMPLATES_PATH`. This allows users to use their own custom app templates in Blender.
___
TrayPublisher & StandalonePublisher: Specify version #5142
Simple creators in TrayPublisher can affect which version will be integrated. Standalone publisher respects the version change from UI.
___
Workfile Builder UI: Workfile builder window is not modal #5131
Workfile Templates Builder:
- Create dialog is not a modal dialog
- Create dialog remains open after create, so you can directly create a new placeholder with similar settings
- In Maya allow to create root level placeholders (no selection during create) - **this felt more like a bugfix than anything else.**
___
3dsmax: Use custom modifiers to hold instance members #4931
Moving logic to handle members of publishing instance from children/parent relationship on Container to tracking via custom attribute on modifier. This eliminates limitations where you couldn't have one node multiple times under one Container and because it stores those relationships as weak references, they are easily transferable even when original nodes are renamed.
___
Add height, width and fps setup to project manager #5075
Add Width, Height, FPS, Pixel Aspect and Frame Start/End to the Project creation dialogue in the Project Manager.I understand that the Project manager will be replaced in the upcoming Ayon, but for the time being I believe setting new project with these options available would be more fun.
___
Nuke: connect custom write node script to the OP setting #5113
Allows user to customize the values of knobs attribute in the OP setting and use it in custom write node
___
Keep `publisher.create_widget` variant when creating subsets #5119
Whenever a person is creating a subset to publish, the "creator" widget resets (where you choose the variant, product, etc.) so if the person is publishing several images of the a variant which is not the default one, they have to keep selecting the correct one after every "create".
This commit resets the original variant upon successful creation of a subset for publishing.
Demo:
[Screencast from 2023-06-08 10-46-40.webm](https://github.com/ynput/OpenPype/assets/1800151/ca1c91d4-b8f3-43d2-a7b7-35987f5b6a3f)
## Testing notes:
1. Launch AYON/OP
2. Launch the publisher (select a project, shot, etc.)
3. Crete a publish type (any works)
4. Choose a variant for the publish that is not the default
5. "Create >>"
The Variant fields should still have the variant you choose.
___
Color Management- added color management support for simple expected files on Deadline #5122
Running of `ExtractOIIOTranscode` during Deadline publish was previously implemented only on DCCs with AOVs (Maya, Max).This PR extends this for other DCCs with flat structure of expected files.
___
hide macos dock icon on build #5133
Set `LSUIElement` to `1` in the `Info.plist` to hide OP icon from the macos dock by default.
___
Pack project: Raise exception with reasonable message #5145
Pack project crashes with relevant message when destination directory is not set.
___
Allow "inventory" actions to be supplied by a Module/Addon. #5146
Adds "inventory" as a possible key to the plugin paths to be returned from a module.
___
3dsmax: make code compatible with 3dsmax 2022 #5164
Python 3.7 in 3dsmax 2022 is not supporting walrus operator. This is removing it from the code for the sake of compatibility
___
Maya: Support same attribute names on different node types. #5054
When validating render settings attributes, support same attribute names on different node types.
___
Maya: bug fix the standin being not loaded when they are first loaded #5143
fix the bug of raising error when the first two standins are loaded through the loaderThe bug mentioned in the related issue: https://github.com/ynput/OpenPype/issues/5129For some reason, `defaultArnoldRenderOptions.operator` is not listed in the connection node attribute even if `cmds.loadPlugin("mtoa", quiet=True)` executed before loading the object as standins for the first time.But if you manually turn on mtoa through plugin preference and load the standins for the first time, it won't raise the related `defaultArnoldRenderOptions.operator` error.
___
Maya: bug fix arnoldExportAss unable to export selected set members #5150
See #5108 fix the bug arnoldExportAss being not able to export and error out during extraction.
___
Maya: Xgen multiple descriptions on single shape - OP-6039 #5160
When having multiple descriptions on the same geometry, the extraction would produce redundant duplicate geometries.
___
Maya: Xgen export of Abc's during Render Publishing - OP-6206 #5167
Shading assignments was missing duplicating the setup for Xgen publishing and the exporting of patches was getting the end frame incorrectly.
___
Maya: Include handles - OP-6236 #5175
Render range was missing the handles.
___
OCIO: Support working with single frame renders #5053
When there is only 1 file, the datamember `files` on the representation should be a string.
___
Burnins: Refactored burnins script #5094
Refactored list value for burnins and fixed command length limit by using temp file for filters string.
___
Nuke: open_file function can open autosave script #5107
Fix the bug of the workfile dialog being unable to open autosave nuke script
___
ImageIO: Minor fixes #5147
Resolve few minor fixes related to latest image io changes from PR.
___
Publisher: Fix save shortcut #5148
Save shortcut should work for both PySide2 and PySide6.
___
Pack Project: Fix files packing #5154
Packing of project with files does work again.
___
Maya: Xgen version mismatch after publish - OP-6204 #5161
Xgen was not updating correctly when for example adding or removing descriptions. This resolve the issue by overwritting the workspace xgen file.
___
Publisher: Edge case fixes #5165
Fix few edge case issues that may cause issues in Publisher UI.
___
Colorspace: host config path backward compatibility #5166
Old project settings overrides are now fully backward compatible. The issue with host config paths overrides were solved and now once a project used to be set to ocio_config **enabled** with found filepaths - this is now considered as activated host ocio_config paths overrides.Nuke is having an popup dialogue which is letting know to a user that settings for config path were changed.
___
Maya: import workfile missing - OP-6233 #5174
Missing `workfile` family to import.
___
Ftrack: Fix ignore sync filter #5176
Ftrack ignore filter does not crash because of dictionary modifications during it's iteration.
___
Webpublisher - headless publish shouldn't be blocking operation #5177
`subprocess.call` was blocking, which resulted in UI non responsiveness as it was waiting for publish to finish.
___
Publisher: Fix disappearing actions #5184
Pyblish plugin actions are visible as expected.
___
Enhancement:animation family loaded as standing (abc) uses "use file sequence" #5110
The changes are the following. We started by updating the the is_sequence(files) function allowing it to return True for a list of files which has only one file, since our animation in this provides just one alembic file. For the correct FPS number, we got the fps from the published ass/abc from the version data.
___
add label to matching family #5128
I added the possibility to filter the `family smart select` with the label in addition to the family.
___
ImageIO: Adding ImageIO activation toggle to all hosts #4700
Colorspace management can now be enabled at the project level, although it is disabled by default. Once enabled, all hosts will use the OCIO config file defined in the settings. If settings are disabled, the system switches to DCC's native color space management, and we do not store colorspace information at the representative level.
___
Redshift Proxy Support in 3dsMax #4625
Redshift Proxy Support for 3dsMax.
- [x] Creator
- [x] Loader
- [x] Extractor
- [x] Validator
- [x] Add documentation
___
Houdini farm publishing and rendering #4825
Deadline Farm publishing and Rendering for Houdini
- [x] Mantra
- [x] Karma(including usd renders)
- [x] Arnold
- [x] Elaborate Redshift ROP for deadline submission
- [x] fix the existing bug in Redshift ROP
- [x] Vray
- [x] add docs
___
Feature: Blender hook to execute python scripts at launch #4905
Hook to allow hooks to add path to a python script that will be executed when Blender starts.
___
Feature: Resolve: Open last workfile on launch through .scriptlib #5047
Added implementation to Resolve integration to open last workfile on launch.
___
General: Remove default windowFlags from publisher #5089
The default windowFlags is making the publisher window (in Linux at least) only show the close button and it's frustrating as many times you just want to minimize the window and get back to the validation after. Removing that line I get what I'd expect.**Before:****After:**
___
General: Show user who created the workfile on the details pane of workfile manager #5093
New PR for https://github.com/ynput/OpenPype/pull/5087, which was closed after merging `next-minor` branch and then realizing we don't need to target it as it was decided it's not required to support windows. More info on that PR discussion.Small addition to add name of the `user` who created the workfile on the details pane of the workfile manager:
___
Loader: Hide inactive versions in UI #5100
Hide versions with `active` set to `False` in Loader UI.
___
Maya: Repair RenderPass token when merging AOVs. #5055
Validator was flagging that `Maya: Improve error feedback when no renderable cameras exist for ASS family. #5092
When collecting cameras for `ass` family, this improves the error message when no cameras are renderable.
___
Nuke: Custom script to set frame range of read nodes #5039
Adding option to set frame range specifically for the read nodes in Openpype Panel. User can set up their preferred frame range with the frame range dialog, which can be showed after clicking `Set Frame Range (Read Node)` in Openpype Tools
___
Update extract review letterbox docs #5074
Update Extract Review - Letter Box section in Docs. Letterbox type description is removed.
___
Project pack: Documents only skips roots validation #5082
Single roots validation is skipped if only documents are extracted.
___
Nuke: custom settings for write node without publish #5084
Set Render Output and other settings to write nodes for non-publish purposes.
___
Maya: Deadline servers #5052
Fix working with multiple Deadline servers in Maya.
- Pools (primary and secondary) attributes were not recreated correctly.
- Order of collector plugins were wrong, so collected data was not injected into render instances.
- Server attribute was not converted to string so comparing with settings was incorrect.
- Improve debug logging for where the webservice url is getting fetched from.
___
Maya: Fix Load Reference. #5091
Fix bug introduced with https://github.com/ynput/OpenPype/pull/4751 where `cmds.ls` returns a list.
___
3dsmax: Publishing Deadline jobs from RedShift #4960
Fix the bug of being uable to publish deadline jobs from RedshiftUse Current File instead of Published Scene for just Redshift.
- add save scene before rendering to ensure the scene is saved after the modification.
- add separated aov files option to allow users to choose to have aovs in render output
- add validator for render publish to aovid overriding the previous renders
___
Houdini: Fix missing frame range for pointcache and camera exports #5026
Fix missing frame range for pointcache and camera exports on published version.
___
Global: collect_frame_fix plugin fix and cleanup #5064
Previous implementation https://github.com/ynput/OpenPype/pull/5036 was broken this is fixing the issue where attribute is found in instance data although the settings were disabled for the plugin.
___
Hiero: Fix apply settings Clip Load #5073
Changed `apply_settings` to classmethod which fixes the issue with settings.
___
Resolve: Make sure scripts dir exists #5078
Make sure the scripts directory exists before looping over it's content.
___
removing info knob from nuke creators #5083
- removing instance node if removed via publisher
- removing info knob since it is not needed any more (was there only for the transition phase)
___
Tray: Fix restart arguments on update #5085
Fix arguments on restart.
___
Maya: bug fix on repair action in Arnold Scene Source CBID Validator #5096
Fix the bug of not being able to use repair action in Arnold Scene Source CBID Validator
___
Nuke: batch of small fixes #5103
- default settings for `imageio.requiredNodes` **CreateWriteImage**
- default settings for **LoadImage** representations
- **Create** and **Publish** menu items with `parent=main_window` (version > 14)
___
Deadline: make prerender check safer #5104
Prerender wasn't correctly recognized and was replaced with just 'render' family.In Nuke it is correctly `prerender.farm` in families, which wasn't handled here. It resulted into using `render` in templates even if `render` and `prerender` templates were split.
___
General: Sort launcher actions alphabetically #5106
The launcher actions weren't being sorted by its label but its name (which on the case of the apps it's the version number) and thus the order wasn't consistent and we kept getting a different order on every launch. From my debugging session, this was the result of what the `actions` variable held after the `filter_compatible_actions` function before these changes:
```
(Pdb) for p in actions: print(p.order, p.name)
0 14-02
0 14-02
0 14-02
0 14-02
0 14-02
0 19-5-493
0 2023
0 3-41
0 6-01
```This caused already a couple bugs from our artists thinking they had launched Nuke X and instead launched Nuke and telling us their Nuke was missing nodes**Before:****After:**
___
TrayPublisher: Editorial video stream discovery #5120
Editorial create plugin in traypublisher does not expect that first stream in input is video.
___
3dsmax: Move from deprecated interface #5117
`INewPublisher` interface is deprecated, this PR is changing the use to `IPublishHost` instead.
___
add movalex as a contributor for code #5076
Adds @movalex as a contributor for code.
This was requested by mkolar [in this comment](https://github.com/ynput/OpenPype/pull/4916#issuecomment-1571498425)
[skip ci]
___
3dsmax: refactor load plugins #5079
___
Blender: Implemented Loading of Alembic Camera #4990
Implemented loading of Alembic cameras in Blender.
___
Unreal: Implemented Creator, Loader and Extractor for Levels #5008
Creator, Loader and Extractor for Unreal Levels have been implemented.
___
Blender: Added setting for base unit scale #4987
A setting for the base unit scale has been added for Blender.The unit scale is automatically applied when opening a file or creating a new one.
___
Unreal: Changed naming and path of Camera Levels #5010
The levels created for the camera in Unreal now include `_camera` in the name, to be better identifiable, and are placed in the camera folder.
___
Settings: Added option to nest settings templates #5022
It is possible to nest settings templates in another templates.
___
Enhancement/publisher: Remove "hit play to continue" label on continue #5029
Remove "hit play to continue" message on continue so that it doesn't show anymore when play was clicked.
___
Ftrack: Limit number of ftrack events to query at once #5033
Limit the amount of ftrack events received from mongo at once to 100.
___
General: Small code cleanups #5034
Small code cleanup and updates.
___
Global: collect frames to fix with settings #5036
Settings for `Collect Frames to Fix` will allow disable per project the plugin. Also `Rewriting latest version` attribute is hiddable from settings.
___
General: Publish plugin apply settings can expect only project settings #5037
Only project settings are passed to optional `apply_settings` method, if the method expects only one argument.
___
Maya: Load Assembly fix invalid imports #4859
Refactors imports so they are now correct.
___
Maya: Skipping rendersetup for members. #4973
When publishing a `rendersetup`, the objectset is and should be empty.
___
Maya: Validate Rig Output IDs #5016
Absolute names of node were not used, so plugin did not fetch the nodes properly.Also missed pymel command.
___
Deadline: escape rootless path in publish job #4910
If the publish path on Deadline job contains spaces or other characters, command was failing because the path wasn't properly escaped. This is fixing it.
___
General: Company name and URL changed #4974
The current records were obsolete in inno_setup, changed to the up-to-date.
___
Unreal: Fix usage of 'get_full_path' function #5014
This PR changes all the occurrences of `get_full_path` functions to alternatives to get the path of the objects.
___
Unreal: Fix sequence frames validator to use correct data #5021
Fix sequence frames validator to use clipIn and clipOut data instead of frameStart and frameEnd.
___
Unreal: Fix render instances collection to use correct data #5023
Fix render instances collection to use `frameStart` and `frameEnd` from the Project Manager, instead of the sequence's ones.
___
Resolve: loader is opening even if no timeline in project #5025
Loader is opening now even no timeline is available in a project.
___
nuke: callback for dirmapping is on demand #5030
Nuke was slowed down on processing due this callback. Since it is disabled by default it made sense to add it only on demand.
___
Publisher: UI works with instances without label #5032
Publisher UI does not crash if instance don't have filled 'label' key in instance data.
___
Publisher: Call explicitly prepared tab methods #5044
It is not possible to go to Create tab during publishing from OpenPype menu.
___
Ftrack: Role names are not case sensitive in ftrack event server status action #5058
Event server status action is not case sensitive for role names of user.
___
Publisher: Fix border widget #5063
Fixed border lines in Publisher UI to be painted correctly with correct indentation and size.
___
Unreal: Fix Commandlet Project and Permissions #5066
Fix problem when creating an Unreal Project when Commandlet Project is in a protected location.
___
Unreal: Added verification for Unreal app name format #5070
The Unreal app name is used to determine the Unreal version folder, so it is necessary that if follows the format `x-x`, where `x` is any integer. This PR adds a verification that the app name follows that format.
___
Docs: Display wrong image in ExtractOIIOTranscode #5045
Wrong image display in `https://openpype.io/docs/project_settings/settings_project_global#extract-oiio-transcode`.
___
Drop-down menu to list all families in create placeholder #4928
Currently in the create placeholder window, we need to write the family manually. This replace the text field by an enum field with all families for the current software.
___
add sync to specific projects or listen only #4919
Extend kitsu sync service with additional arguments to sync specific projects.
___
Publisher: Show instances in report page #4915
Show publish instances in report page. Also added basic log view with logs grouped by instance. Validation error detail now have 2 colums, one with erro details second with logs. Crashed state shows fast access to report action buttons. Success will show only logs. Publish frame is shrunked automatically on publish stop.
___
Fusion - Loader plugins updates #4920
Update to some Fusion loader plugins:The sequence loader can now load footage from the image and online family.The FBX loader can now import all formats Fusions FBX node can read.You can now import the content of another workfile into your current comp with the workfile loader.
___
Fusion: deadline farm rendering #4955
Enabling Fusion for deadline farm rendering.
___
AfterEffects: set frame range and resolution #4983
Frame information (frame start, duration, fps) and resolution (width and height) is applied to selected composition from Asset Management System (Ftrack or DB) automatically when published instance is created.It is also possible explicitly propagate both values from DB to selected composition by newly added menu buttons.
___
Publish: Enhance automated publish plugin settings #4986
Added plugins option to define settings category where to look for settings of a plugin and added public helper functions to apply settings `get_plugin_settings` and `apply_plugin_settings_automatically`.
___
Load Rig References - Change Rig to Animation in Animation instance #4877
We are using the template builder to build an animation scene. All the rig placeholders are imported correctly, but the automatically created animation instances retain the rig family in their names and subsets. In our example, we need animationMain instead of rigMain, because this name will be used in the following steps like lighting.Here is the result we need. I checked, and it's not a template builder problem, because even if I load a rig as a reference, the result is the same. For me, since we are in the animation instance, it makes more sense to have animation instead of rig in the name. The naming is just fine if we use create from the Openpype menu.
___
Enhancement: Resolve prelaunch code refactoring and update defaults #4916
The main reason of this PR is wrong default settings in `openpype/settings/defaults/system_settings/applications.json` for Resolve host. The `bin` folder should not be a part of the macos and Linux `RESOLVE_PYTHON3_PATH` variable.The rest of this PR is some code cleanups for Resolve prelaunch hook to simplify further development.Also added a .gitignore for vscode workspace files.
___
Unreal: 🚚 move Unreal plugin to separate repository #4980
To support Epic Marketplace have to move AYON Unreal integration plugins to separate repository. This is replacing current files with git submodule, so the change should be functionally without impact.New repository lives here: https://github.com/ynput/ayon-unreal-plugin
___
General: Lib code cleanup #5003
Small cleanup in lib files in openpype.
___
Allow to open with djv by extension instead of representation name #5004
Filter open in djv action by extension instead of representation.
___
DJV open action `extensions` as `set` #5005
Change `extensions` attribute to `set`.
___
Nuke: extract thumbnail with multiple reposition nodes #5011
Added support for multiple reposition nodes.
___
Enhancement: Improve logging levels and messages for artist facing publish reports #5018
Tweak the logging levels and messages to try and only show those logs that an artist should see and could understand. Move anything that's slightly more involved into a "debug" message instead.
___
Bugfix/frame variable fix #4978
Renamed variables to match OpenPype terminology to reduce confusion and add consistency.
___
Global: plugins cleanup plugin will leave beauty rendered files #4790
Attempt to mark more files to be cleaned up explicitly in intermediate `renders` folder in work area for farm jobs.
___
Fix: Download last workfile doesn't work if not already downloaded #4942
Some optimization condition is messing with the feature: if the published workfile is not already downloaded, it won't download it...
___
Unreal: Fix transform when loading layout to match existing assets #4972
Fixed transform when loading layout to match existing assets.
___
fix the bug of fbx loaders in Max #4977
bug fix of fbx loaders for not being able to parent to the CON instances while importing cameras(and models) which is published from other DCCs such as Maya.
___
AfterEffects: allow returning stub with not saved workfile #4984
Allows to use Workfile app to Save first empty workfile.
___
Blender: Fix Alembic loading #4985
Fixed problem occurring when trying to load an Alembic model in Blender.
___
Unreal: Addon Py2 compatibility #4994
Fixed Python 2 compatibility of unreal addon.
___
Nuke: fixed missing files key in representation #4999
Issue with missing keys once rendering target set to existing frames is fixed. Instance has to be evaluated in validation for missing files.
___
Unreal: Fix the frame range when loading camera #5002
The keyframes of the camera, when loaded, were not using the correct frame range.
___
Fusion: fixing frame range targeting #5013
Frame range targeting at Rendering instances is now following configured options.
___
Deadline: fix selection from multiple webservices #5015
Multiple different DL webservice could be configured. First they must by configured in System Settings., then they could be configured per project in `project_settings/deadline/deadline_servers`.Only single webservice could be a target of publish though.
___
3dsmax: Refactored publish plugins to use proper implementation of pymxs #4988
___
Addons directory #4893
This adds a directory for Addons, for easier distribution of studio specific code.
___
Kitsu - Add "image", "online" and "plate" to review families #4923
This PR adds "image", "online" and "plate" to the review families so they also can be uploaded to Kitsu.It also adds the `Add review to Kitsu` tag to the default png review. Without it the user would manually need to add it for single image uploads to Kitsu and might confuse users (it confused me first for a while as movies did work).
___
Feature/remove and load inv action #4930
Added the ability to remove and load a container, as a way to reset it.This can be useful in cases where a container breaks in a way that can be fixed by removing it, then reloading it.Also added the ability to add `InventoryAction` plugins by placing them in `openpype/plugins/inventory`.
___
Load Rig References - Change Rig to Animation in Animation instance #4877
We are using the template builder to build an animation scene. All the rig placeholders are imported correctly, but the automatically created animation instances retain the rig family in their names and subsets. In our example, we need animationMain instead of rigMain, because this name will be used in the following steps like lighting.Here is the result we need. I checked, and it's not a template builder problem, because even if I load a rig as a reference, the result is the same. For me, since we are in the animation instance, it makes more sense to have animation instead of rig in the name. The naming is just fine if we use create from the Openpype menu.
___
Maya template builder - preserve all references when importing a template #4797
When building a template with Maya template builder, we import the template and also the references inside the template file. This causes some problems:
- We cannot use the references to version assets imported by the template.
- When we import the file, the internal reference files are also imported. As a side effect, Maya complains about a reference that no longer exists.`// Error: file: /xxx/maya/2023.3/linux/scripts/AETemplates/AEtransformRelated.mel line 58: Reference node 'turntable_mayaSceneMain_01_RN' is not associated with a reference file.`
___
Unreal: Renaming the integration plugin to Ayon. #4646
Renamed the .h, and .cpp files to Ayon. Also renamed the classes to with the Ayon keyword.
___
3dsMax: render dialogue needs to be closed #4729
Make sure the render setup dialog is in a closed state for the update of resolution and other render settings
___
Maya Template Builder - Remove default cameras from renderable cameras #4815
When we build an asset workfile with build workfile from template inside Maya, we load our turntable camera. But then we end up with 2 renderables camera : **persp** the one imported from the template.We need to remove the **persp** camera (or any other default camera) from renderable cameras when building the work file.
___
Validators for Frame Range in Max #4914
Switch Render Frame Range Type to 3 for specific ranges (initial setup for the range type is 4)Reset Frame Range will also set the frame range for render settingsRender Collector won't take the frame range from context data but take the range directly from render settingAdd validators for render frame range type and frame range respectively with repair action
___
Fusion: Saver creator settings #4943
Adding Saver creator settings and enhanced rendering path with template.
___
General: Project Anatomy on creators #4962
Anatomy object of current project is available on `CreateContext` and create plugins.
___
Maya: Validate shader name - OP-5903 #4971
Running the plugin would error with:
```
// TypeError: 'str' object cannot be interpreted as an integer
```Fixed and added setting `active`.
___
Houdini: Fix slow Houdini launch due to shelves generation #4829
Shelf generation during Houdini startup would add an insane amount of delay for the Houdini UI to launch correctly. By deferring the shelf generation this takes away the 5+ minutes of delay for the Houdini UI to launch.
___
Fusion - Fixed "optional validation" #4912
Added OptionalPyblishPluginMixin and is_active checks for all publish tools that should be optional
___
Bug: add missing `pyblish.util` import #4937
remote publishing was missing import of `remote_publish`. This is adding it back.
___
Unreal: Fix missing 'object_path' property #4938
Epic removed the `object_path` property from `AssetData`. This PR fixes usages of that property.Fixes #4936
___
Remove obsolete global validator #4939
Removing `Validate Sequence Frames` validator from global plugins as it wasn't handling correctly many things and was by mistake enabled, breaking functionality on Deadline.
___
General: fix build_workfile get_linked_assets missing project_name arg #4940
Linked assets collection don't work within `build_workfile` because `get_linked_assets` function call has a missing `project_name`argument.
- Added the `project_name` arg to the `get_linked_assets` function call.
___
General: fix Scene Inventory switch version error dialog missing parent arg on init #4941
QuickFix for the switch version error dialog to set inventory widget as parent.
___
Unreal: Fix camera frame range #4956
Fix the frame range of the level sequence for the Camera in Unreal.
___
Unreal: Fix missing parameter when updating Alembic StaticMesh #4957
Fix an error when updating an Alembic StaticMesh in Unreal, due to a missing parameter in a function call.
___
Unreal: Fix render extraction #4963
Fix a problem with the extraction of renders in Unreal.
___
Unreal: Remove Python 3.8 syntax from addon #4965
Removed Python 3.8 syntax from addon.
___
Ftrack: Fix editorial task creation #4966
Fix key assignment on instance data during editorial publishing in ftrack hierarchy integration.
___
Add "shortcut" to Scripts Menu Definition #4927
Add the possibility to associate a shorcut for an entry in the script menu definition with the key "shortcut"
___
Substance Painter Integration #4283
This implements a part of #4205 by implementing a Substance Painter integration
Status:
- [x] Implement Host
- [x] start substance with last workfile using `AddLastWorkfileToLaunchArgs` prelaunch hook
- [x] Implement Qt tools
- [x] Implement loaders
- [x] Implemented a Set project mesh loader (this is relatively special case because a Project will always have exactly one mesh - a Substance Painter project cannot exist without a mesh).
- [x] Implement project open callback
- [x] On project open it notifies the user if the loaded model is outdated
- [x] Implement publishing logic
- [x] Workfile publishing
- [x] Export Texture Sets
- [x] Support OCIO using #4195 (draft brach is set up - see comment)
- [ ] Likely needs more testing on the OCIO front
- [x] Validate all outputs of the Export template are exported/generated
- [x] Allow validation to be optional **(issue: there's no API method to detect what maps will be exported without doing an actual export to disk)**
- [x] Support extracting/integration if not all outputs are generated
- [x] Support multiple materials/texture sets per instance
- [ ] Add validator that can enforce only a single texture set output if studio prefers that.
- [ ] Implement Export File Format (extensions) override in Creator
- [ ] Add settings so Admin can choose which extensions are available.
___
Data Exchange: Geometry in 3dsMax #4555
Introduces and updates a creator, extractors and loaders for model family
Introduces new creator, extractors and loaders for model family while adding model families into the existing max scene loader and extractor
- [x] creators
- [x] adding model family into max scene loader and extractor
- [x] fbx loader
- [x] fbx extractor
- [x] usd loader
- [x] usd extractor
- [x] validator for model family
- [x] obj loader(update function)
- [x] fix the update function of the loader as #4675
- [x] Add documentation
___
AfterEffects: add review flag to each instance #4884
Adds `mark_for_review` flag to the Creator to allow artists to disable review if necessary.Exposed this flag in Settings, by default set to True (eg. same behavior as previously).
___
Houdini: Fix Validate Output Node (VDB) #4819
- Removes plug-in that was a duplicate of this plug-in.
- Optimize logging of many prims slightly
- Fix error reporting like https://github.com/ynput/OpenPype/pull/4818 did
___
Houdini: Add null node as output indicator when using TAB search #4834
___
Houdini: Don't error in collect review if camera is not set correctly #4874
Do not raise an error in collector when invalid path is set as camera path. Allow camera path to not be set correctly in review instance until validation so it's nicely shown in a validation report.
___
Project packager: Backup and restore can store only database #4879
Pack project functionality have option to zip only project database without project files. Unpack project can skip project copy if the folder is not found.Added helper functions to `openpype.client.mongo` that can be also used for tests as replacement of mongo dump.
___
Houdini: ExtractOpenGL for Review instance not optional #4881
Don't make ExtractOpenGL optional for review instance optional.
___
Publisher: Small style changes #4894
Small changes in styles and form of publisher UI.
___
Houdini: Workfile icon in new publisher #4898
Fix icon for the workfile instance in new publisher
___
Fusion: Simplify creator icons code #4899
Simplify code for setting the icons for the Fusion creators
___
Enhancement: Fix PySide 6.5 support for loader #4900
Fixes PySide 6.5 support in Loader.
___
Maya: Validate Attributes #4917
This plugin was broken due to bad fetching of data and wrong repair action.
___
Fix: Locally copied version of last published workfile is not incremented #4722
### Fix 1
When copied, the local workfile version keeps the published version number, when it must be +1 to follow OP's naming convention.
### Fix 2
Local workfile version's name is built from anatomy. This avoids to get workfiles with their publish template naming.
### Fix 3
In the case a subset has at least two tasks with published workfiles, for example `Modeling` and `Rigging`, launching `Rigging` was getting the first one with the `next` and trying to find representations, therefore `workfileModeling` and trying to match the current `task_name` (`Rigging`) with the `representation["context"]["task"]["name"]` of a Modeling representation, which was ending up to a `workfile_representation` to `None`, and exiting the process.
Trying to find the `task_name` in the `subset['name']` fixes it.
### Fix 4
Fetch input dependencies of workfile.
Replacing https://github.com/ynput/OpenPype/pull/4102 for changes to bring this home.
___
Maya: soft-fail when pan/zoom locked on camera when playblasting #4929
When pan/zoom enabled attribute on camera is locked, playblasting with pan/zoom fails because it is trying to restore it. This is fixing it by skipping over with warning.
___
Maya Load References - Add Display Handle Setting #4904
When we load a reference in Maya using OpenPype loader, display handle is checked by default and prevent us to select easily the object in the viewport. I understand that some productions like to keep this option, so I propose to add display handle to the reference loader settings.
___
Photoshop: add autocreators for review and flat image #4871
Review and flatten image (produced when no instance of `image` family was created) were created somehow magically. This PRintroduces two new auto creators which allow artists to disable review or flatten image.For all `image` instances `Review` flag was added to provide functionality to create separate review per `image` instance. Previously was possible only to have separate instance of `review` family.Review is not enabled on `image` family by default. (Eg. follows original behavior)Review auto creator is enabled by default as it was before.Flatten image creator must be set in Settings in `project_settings/photoshop/create/AutoImageCreator`.
___
Maya: Playblast profiles #4777
Support playblast profiles.This enables studios to customize what playblast settings should be on a per task and/or subset basis. For example `modeling` should have `Wireframe On Shaded` enabled, while all other tasks should have it disabled.
___
Maya: Support .abc files directly for Arnold standin look assignment #4856
If `.abc` file is loaded into arnold standin support look assignment through the `cbId` attributes in the alembic file.
___
Maya: Hide animation instance in creator #4872
- Hide animation instance in creator
- Add inventory action to recreate animation publish instance for loaded rigs
___
Unreal: Render Creator enhancements #4477
Improvements to the creator for render family
This PR introduces some enhancements to the creator for the render family in Unreal Engine:
- Added the option to create a new, empty sequence for the render.
- Added the option to not include the whole hierarchy for the selected sequence.
- Improvements of the error messages.
___
Unreal: Added settings for rendering #4575
Added settings for rendering in Unreal Engine.
Two settings has been added:
- Pre roll frames, to set how many frames are used to load the scene before starting the actual rendering.
- Configuration path, to allow to save a preset of settings from Unreal, and use it for rendering.
___
Global: Optimize anatomy formatting by only formatting used templates instead #4784
Optimization to not format full anatomy when only a single template is used. Instead format only the single template instead.
___
Patchelf version locked #4853
For Centos dockerfile it is necessary to lock the patchelf version to the older, otherwise the build process fails.
___
Houdini: Implement `switch` method on loaders #4866
Implement `switch` method on loaders
___
Code: Tweak docstrings and return type hints #4875
Tweak docstrings and return type hints for functions in `openpype.client.entities`.
___
Publisher: Clear comment on successful publish and on window close #4885
Clear comment text field on successful publish and on window close.
___
Publisher: Make sure to reset asset widget when hidden and reshown #4886
Make sure to reset asset widget when hidden and reshown. Without this the asset list would never refresh in the set asset widget when changing context on an existing instance and thus would not show new assets from after the first time launching that widget.
___
Maya: Fix nested model instances. #4852
Fix nested model instance under review instance, where data collection was not including "Display Lights" and "Focal Length".
___
Maya: Make default namespace naming backwards compatible #4873
Namespaces of loaded references are now _by default_ back to what they were before #4511
___
Nuke: Legacy convertor skips deprecation warnings #4846
Nuke legacy convertor was triggering deprecated function which is causing a lot of logs which slows down whole process. Changed the convertor to skip all nodes without `AVALON_TAB` to avoid the warnings.
___
3dsmax: move startup script logic to hook #4849
Startup script for OpenPype was interfering with Open Last Workfile feature. Moving this loggic from simple command line argument in the Settings to pre-launch hook is solving the order of command line arguments and making both features work.
___
Maya: Don't change time slider ranges in `get_frame_range` #4858
Don't change time slider ranges in `get_frame_range`
___
Maya: Looks - calculate hash for tx texture #4878
Texture hash is calculated for textures used in published look and it is used as key in dictionary. In recent changes, this hash is not calculated for TX files, resulting in `None` value as key in dictionary, crashing publishing. This PR is adding texture hash for TX files to solve that issue.
___
Houdini: Collect `currentFile` context data separate from workfile instance #4883
Fix publishing without an active workfile instance due to missing `currentFile` data.Now collect `currentFile` into context in houdini through context plugin no matter the active instances.
___
Nuke: fixed broken slate workflow once published on deadline #4887
Slate workflow is now working as expected and Validate Sequence Frames is not raising the once slate frame is included.
___
Add fps as instance.data in collect review in Houdini. #4888
fix the bug of failing to publish extract review in HoudiniOriginal error:
```python
File "OpenPype\build\exe.win-amd64-3.9\openpype\plugins\publish\extract_review.py", line 516, in prepare_temp_data
"fps": float(instance.data["fps"]),
KeyError: 'fps'
```
___
TrayPublisher: Fill missing data for instances with review #4891
Fill required data to instance in traypublisher if instance has review family. The data are required by ExtractReview and it would be complicated to do proper fix at this moment! The collector does for review instances what did https://github.com/ynput/OpenPype/pull/4383
___
Publisher: Keep track about current context and fix context selection widget #4892
Change selected context to current context on reset. Fix bug when context widget is re-enabled.
___
Scene inventory: Model refresh fix with cherry picking #4895
Fix cherry pick issue in scene inventory.
___
Nuke: Pre-render and missing review flag on instance causing crash #4897
If instance created in nuke was missing `review` flag, collector crashed.
___
After Effects: fix handles KeyError #4727
Sometimes when publishing with AE (we only saw this error on AE 2023), we got a KeyError for the handles in the "Collect Workfile" step. So I did get the handles from the context if ther's no handles in the asset entity.
___
Maya: Cant assign shaders to the ass file - OP-4859 #4460
Support AiStandIn nodes for look assignment.
Using operators we assign shaders and attribute/parameters to nodes within standins. Initially there is only support for a limited mount of attributes but we can add support as needed;
```
primaryVisibility
castsShadows
receiveShadows
aiSelfShadows
aiOpaque
aiMatte
aiVisibleInDiffuseTransmission
aiVisibleInSpecularTransmission
aiVisibleInVolume
aiVisibleInDiffuseReflection
aiVisibleInSpecularReflection
aiSubdivUvSmoothing
aiDispHeight
aiDispPadding
aiDispZeroValue
aiStepSize
aiVolumePadding
aiSubdivType
aiSubdivIterations
```
___
Maya: GPU cache representation #4649
Implement GPU cache for model, animation and pointcache.
___
Houdini: Implement review family with opengl node #3839
Implements a first pass for Reviews publishing in Houdini. Resolves #2720
Uses the `opengl` ROP node to produce PNG images.
___
Maya: Camera focal length visible in review - OP-3278 #4531
Camera focal length visible in review.
Support camera focal length in review; static and dynamic.Resolves #3220
___
Maya: Defining plugins to load on Maya start - OP-4994 #4714
Feature to define plugins to load on Maya launch.
___
Nuke, DL: Returning Suspended Publishing attribute #4715
Old Nuke Publisher's feature for suspended publishing job on render farm was added back to the current Publisher.
___
Settings UI: Allow setting a size hint for text fields #4821
Text entity have `minimum_lines_count` which allows to change minimum size hint of UI input.
___
TrayPublisher: Move 'BatchMovieCreator' settings to 'create' subcategory #4827
Moved settings for `BatchMoviewCreator` into subcategory `create` in settings. Changes are made to match other hosts settings chema and structure.
___
Maya looks: support for native Redshift texture format #2971
Add support for native Redshift textures handling. Closes #2599
Uses Redshift's Texture Processor executable to convert textures being used in renders to the Redshift ".rstexbin" format.
___
Maya: custom namespace for references #4511
Adding an option in Project Settings > Maya > Loader plugins to set custom namespace. If no namespace is set, the default one is used.
___
Maya: Set correct framerange with handles on file opening #4664
Set the range of playback from the asset data, counting handles, to get the correct data when calling the "collect_animation_data" function.
___
Maya: Fix camera update #4751
Fix resetting any modelPanel to a different camera when loading a camera and updating.
___
Maya: Remove single assembly validation for animation instances #4840
Rig groups may now be parented to others groups when `includeParentHierarchy` attribute on the instance is "off".
___
Maya: Optional control of display lights on playblast. #4145
Optional control of display lights on playblast.
Giving control to what display lights are on the playblasts.
___
Kitsu: note family requirements #4551
Allowing to add family requirements to `IntegrateKitsuNote` task status change.
Adds a `Family requirements` setting to `Integrate Kitsu Note`, so you can add requirements to determine if kitsu task status should be changed based on which families are published or not. For instance you could have the status change only if another subset than workfile is published (but workfile can still be included) by adding an item set to `Not equal` and `workfile`.
___
Deactivate closed Kitsu projects on OP #4619
Deactivate project on OP when the project is closed on Kitsu.
___
Maya: Suggestion to change capture labels. #4691
Change capture labels.
___
Houdini: Change node type for OpenPypeContext `null` -> `subnet` #4745
Change the node type for OpenPype's hidden context node in Houdini from `null` to `subnet`. This fixes #4734
___
General: Extract burnin hosts filters #4749
Removed hosts filter from ExtractBurnin plugin. Instance without representations won't cause crash but just skip the instance. We've discovered because Blender already has review but did not create burnins.
___
Global: Improve speed of Collect Custom Staging Directory #4768
Improve speed of Collect Custom Staging Directory.
___
General: Anatomy templates formatting #4773
Added option to format only single template from anatomy instead of formatting all of them all the time. Formatting of all templates is causing slowdowns e.g. during publishing of hundreds of instances.
___
Harmony: Handle zip files with deeper structure #4782
External Harmony zip files might contain one additional level with scene name.
___
Unreal: Use common logic to configure executable #4788
Unreal Editor location and version was autodetected. This easied configuration in some cases but was not flexible enought. This PR is changing the way Unreal Editor location is set, unifying it with the logic other hosts are using.
___
Github: Grammar tweaks + uppercase issue title #4813
Tweak some of the grammar in the issue form templates.
___
Houdini: Allow creation of publish instances via Houdini TAB menu #4831
Register the available Creator's as houdini tools so an artist can add publish instances via the Houdini TAB node search menu from within the network editor.
___
Maya: Fix Collect Render for V-Ray, Redshift and Renderman for missing colorspace #4650
Fix Collect Render not working for Redshift, V-Ray and Renderman due to missing `colorspace` argument to `RenderProduct` dataclass.
___
Maya: Xgen fixes #4707
Fix for Xgen extraction of world parented nodes and validation for required namespace.
___
Maya: Fix extract review and thumbnail for Maya 2020 #4744
Fix playblasting in Maya 2020 with override viewport options enabled. Fixes #4730.
___
Maya: local variable 'arnold_standins' referenced before assignment - OP-5542 #4778
MayaLookAssigner erroring when MTOA is not loaded:
```
# Traceback (most recent call last):
# File "\openpype\hosts\maya\tools\mayalookassigner\app.py", line 272, in on_process_selected
# nodes = list(set(item["nodes"]).difference(arnold_standins))
# UnboundLocalError: local variable 'arnold_standins' referenced before assignment
```
___
Maya: Fix getting view and display in Maya 2020 - OP-5035 #4795
The `view_transform` returns a different format in Maya 2020. Fixes #4540 (hopefully).
___
Maya: Fix Look Maya 2020 Py2 support for Extract Look #4808
Fix Extract Look supporting python 2.7 for Maya 2020.
___
Maya: Fix Validate Mesh Overlapping UVs plugin #4816
Fix typo in the code where a maya command returns a `list` instead of `str`.
___
Maya: Fix tile rendering with Vray - OP-5566 #4832
Fixes tile rendering with Vray.
___
Deadline: checking existing frames fails when there is number in file name #4698
Previous implementation of validator failed on files with any other number in rendered file names.Used regular expression pattern now handles numbers in the file names (eg "Main_beauty.v001.1001.exr", "Main_beauty_v001.1001.exr", "Main_beauty.1001.1001.exr") but not numbers behind frames (eg. "Main_beauty.1001.v001.exr")
___
Maya: Validate Render Settings. #4735
Fixes error message when using attribute validation.
___
General: Hero version sites recalculation #4737
Sites recalculation in integrate hero version did expect that it is integrated exactly same amount of files as in previous integration. This is not the case in many cases, so the sites recalculation happens in a different way, first are prepared all sites from previous representation files, and all of them are added to each file in new representation.
___
Houdini: Fix collect current file #4739
Fixes the Workfile publishing getting added into every instance being published from Houdini
___
Global: Fix Extract Burnin + Colorspace functions for conflicting python environments with PYTHONHOME #4740
This fixes the running of openpype processes from e.g. a host with conflicting python versions that had `PYTHONHOME` said additionally to `PYTHONPATH`, like e.g. Houdini Py3.7 together with OpenPype Py3.9 when using Extract Burnin for a review in #3839This fix applies to Extract Burnin and some of the colorspace functions that use `run_openpype_process`
___
Harmony: render what is in timeline in Harmony locally #4741
Previously it wasn't possible to render according to what was set in Timeline in scene start/end, just by what it was set in whole timeline.This allows artist to override what is in DB with what they require (with disabled `Validate Scene Settings`). Now artist can extend scene by additional frames, that shouldn't be rendered, but which might be desired.Removed explicit set scene settings (eg. applying frames and resolution directly to the scene after launch), added separate menu item to allow artist to do it themselves.
___
Maya: Extract Review settings add Use Background Gradient #4747
Add Display Gradient Background toggle in settings to fix support for setting flat background color for reviews.
___
Nuke: publisher is offering review on write families on demand #4755
Original idea where reviewable toggle will be offered in publisher on demand is fixed and now `review` attribute can be disabled in settings.
___
Workfiles: keep Browse always enabled #4766
Browse might make sense even if there are no workfiles present, actually in that case it makes the most sense (eg. I want to locate workfile from outside - from Desktop for example).
___
Global: label key in instance data is optional #4779
Collect OTIO review plugin is not crashing if `label` key is missing in instance data.
___
Loader: Fix missing variable #4781
There is missing variable `handles` in loader tool after https://github.com/ynput/OpenPype/pull/4746. The variable was renamed to `handles_label` and is initialized to `None` if handles are not available.
___
Nuke: Workfile Template builder fixes #4783
Popup window after Nuke start is not showing. Knobs with X/Y coordination on nodes where were converted from placeholders are not added if `keepPlaceholders` is witched off.
___
Maya: Add family filter 'review' to burnin profile with focal length #4791
Avoid profile burnin with `focalLength` key for renders, but use only for playblast reviews.
___
add farm instance to the render collector in 3dsMax #4794
bug fix for the failure of submitting publish job in 3dsmax
___
Publisher: Plugin active attribute is respected #4798
Publisher consider plugin's `active` attribute, so the plugin is not processed when `active` is set to `False`. But we use the attribute in `OptionalPyblishPluginMixin` for different purposes, so I've added hack bypass of the active state validation when plugin inherit from the mixin. This is temporary solution which cannot be changed until all hosts use Publisher otherwise global plugins would be broken. Also plugins which have `enabled` set to `False` are filtered out -> this happened only when automated settings were applied and the settings contained `"enabled"` key se to `False`.
___
Nuke: settings and optional attribute in publisher for some validators #4811
New publisher is supporting optional switch for plugins which is offered in Publisher in Right panel. Some plugins were missing this switch and also settings which would offer the optionality.
___
Settings: Version settings popup fix #4822
Version completer popup have issues on some platforms, this should fix those edge cases. Also fixed issue when completer stayed shown fater reset (save).
___
Hiero/Nuke: adding monitorOut key to settings #4826
New versions of Hiero were introduced with new colorspace property for Monitor Out. It have been added into project settings. Also added new config names into settings enumerator option.
___
Nuke: removed default workfile template builder preset #4835
Default for workfile template builder should have been empty.
___
TVPaint: Review can be made from any instance #4843
Add `"review"` tag to output of extract sequence if instance is marked for review. At this moment only instances with family `"review"` were able to define input for `ExtractReview` plugin which is not right.
___
Deadline: Remove unused FramesPerTask job info submission #4657
Remove unused `FramesPerTask` job info submission to Deadline.
___
Maya: Remove pymel dependency #4724
Refactors code written using `pymel` to use standard maya python libraries instead like `maya.cmds` or `maya.api.OpenMaya`
___
Remove "preview" data from representation #4759
Remove "preview" data from representation
___
Maya: Collect Review cleanup code for attached subsets #4720
Refactor some code for Maya: Collect Review for attached subsets.
___
Refactor: Remove `handles`, `edit_in` and `edit_out` backwards compatibility #4746
Removes backward compatibiliy fallback for data called `handles`, `edit_in` and `edit_out`.
___
Bump webpack from 5.69.1 to 5.76.1 in /website #4624
Bumps [webpack](https://github.com/webpack/webpack) from 5.69.1 to 5.76.1.
Release notes
v5.76.1
Fixed
assert/strict built-in to NodeTargetPluginRevert
hashRegExp lookup by @ryanwilsonperkin in webpack/webpack#16759v5.76.0
Bugfixes
@Jack-Works in webpack/webpack#16500@lvivski in webpack/webpack#16491generatedCode info to fix bug in asset module cache restoration by @ryanwilsonperkin in webpack/webpack#16703hashRegExp lookup by @ryanwilsonperkin in webpack/webpack#16759Features
target to LoaderContext type by @askoufis in webpack/webpack#16781Security
@akhilgkrishnan in webpack/webpack#16446Repo Changes
@jakebailey in webpack/webpack#16614@jakebailey in webpack/webpack#16613@piwysocki in webpack/webpack#16493New Contributors
@Jack-Works made their first contribution in webpack/webpack#16500@lvivski made their first contribution in webpack/webpack#16491@jakebailey made their first contribution in webpack/webpack#16614@akhilgkrishnan made their first contribution in webpack/webpack#16446@ryanwilsonperkin made their first contribution in webpack/webpack#16703@piwysocki made their first contribution in webpack/webpack#16493@askoufis made their first contribution in webpack/webpack#16781v5.75.0
Bugfixes
experiments.* normalize to false when opt-outNaN%window before trying to access iteval-nosources-* actually exclude sourcesFeatures
@import to extenal CSS when using experimental CSS in nodeCommits
21be52b Merge pull request #16804 from webpack/chore-patch-release1cce945 chore(release): 5.76.1e76ad9e Merge pull request #16803 from ryanwilsonperkin/revert-16759-real-content-has...52b1b0e Revert "Improve performance of hashRegExp lookup"c989143 Merge pull request #16766 from piranna/patch-1710eaf4 Merge pull request #16789 from dmichon-msft/contenthash-hashsalt5d64468 Merge pull request #16792 from webpack/update-version67af5ec chore(release): 5.76.097b1718 Merge pull request #16781 from askoufis/loader-context-target-typeb84efe6 Merge pull request #16759 from ryanwilsonperkin/real-content-hash-regex-perfMaintainer changes
[](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)
Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`.
[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)
---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually
- `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
- `@dependabot use these labels` will set the current labels as the default for future PRs for this repo and language
- `@dependabot use these reviewers` will set the current reviewers as the default for future PRs for this repo and language
- `@dependabot use these assignees` will set the current assignees as the default for future PRs for this repo and language
- `@dependabot use this milestone` will set the current milestone as the default for future PRs for this repo and language
You can disable automated security fix PRs for this repo from the [Security Alerts page](https://github.com/ynput/OpenPype/network/alerts).
Documentation: Add Extract Burnin documentation #4765
Add documentation for Extract Burnin global plugin settings.
___
Documentation: Move publisher related tips to publisher area #4772
Move publisher related tips for After Effects artist documentation to the correct position.
___
Documentation: Add extra terminology to the key concepts glossary #4838
Tweak some of the key concepts in the documentation.
___
Maya: Refactor Extract Look with dedicated processors for maketx #4711
Refactor Maya extract look to fix some issues:
- [x] Allow Extraction with maketx with OCIO Color Management enabled in Maya.
- [x] Fix file hashing so it includes arguments to maketx, so that when arguments change it correctly generates a new hash
- [x] Fix maketx destination colorspace when OCIO is enabled
- [x] Use pre-collected colorspaces of the resources instead of trying to retrieve again in Extract Look
- [x] Fix colorspace attributes being reinterpreted by maya on export (fix remapping) - goal is to resolve #2337
- [x] Fix support for checking config path of maya default OCIO config (due to using `lib.get_color_management_preferences` which remaps that path)
- [x] Merged in #2971 to refactor MakeTX into TextureProcessor and also support generating Redshift `.rstexbin` files. - goal is to resolve #2599
- [x] Allow custom arguments to `maketx` from OpenPype Settings like mentioned here by @fabiaserra for arguments like: `--monochrome-detect`, `--opaque-detect`, `--checknan`.
- [x] Actually fix the code and make it work. :) (I'll try to keep below checkboxes in sync with my code changes)
- [x] Publishing without texture processor should work (no maketx + no rstexbin)
- [x] Publishing with maketx should work
- [x] Publishing with rstexbin should work
- [x] Test it. (This is just me doing some test-runs, please still test the PR!)
___
Maya template builder load all assets linked to the shot #4761
Problem
All the assets of the ftrack project are loaded and not those linked to the shot
How get error
Open maya in the context of shot, then build a new scene with the "Build Workfile from template" button in "OpenPype" menu.

___
Global: Do not force instance data with frame ranges of the asset #4383
This aims to resolve #4317
___
Cosmetics: Fix some grammar in docstrings and messages (and some code) #4752
Tweak some grammar in codebase
___
Deadline: Submit publish job fails due root work hardcode - OP-5528 #4775
Generating config templates was hardcoded to `root[work]`. This PR fixes that.
___
CreateContext: Added option to remove Unknown attributes #4776
Added option to remove attributes with UnkownAttrDef on instances. Pop of key will also remove the attribute definition from attribute values, so they're not recreated again.
___
Blender: Extract Review #3616
Added Review to Blender.
This implementation is based on #3508 but made compatible for the current implementation of OpenPype for Blender.
___
Data Exchanges: Point Cloud for 3dsMax #4532
Publish PRT format with tyFlow in 3dsmax
Publish PRT format with tyFlow in 3dsmax and possibly set up loader to load the format too.
- [x] creator
- [x] extractor
- [x] validator
- [x] loader
___
Global: persistent staging directory for renders #4583
Allows configure if staging directory (`stagingDir`) should be persistent with use of profiles.
With this feature, users can specify a transient data folder path based on presets, which can be used during the creation and publishing stages. In some cases, these DCCs automatically add a rendering path during the creation stage, which is then used in publishing.One of the key advantages of this feature is that it allows users to take advantage of faster storages for rendering, which can help improve workflow efficiency. Additionally, this feature allows users to keep their rendered data persistent, and use their own infrastructure for regular cleaning.However, it should be noted that some productions may want to use this feature without persistency. Furthermore, there may be a need for retargeting the rendering folder to faster storages, which is also not supported at the moment.It is studio responsibility to clean up obsolete folders with data.Location of the folder is configured in `project_anatomy/templates/others`. ('transient' key is expected, with 'folder' key, could be more templates)Which family/task type/subset is applicable is configured in:`project_settings/global/tools/publish/transient_dir_profiles`
___
Kitsu custom comment template #4599
Kitsu allows to write markdown in its comment field. This can be something very powerful to deliver dynamic comments with the help the data from the instance.This feature is defaults to off so the admin have to manually set up the comment field the way they want.I have added a basic example on how the comment can look like as the comment-fields default value.To this I want to add some documentation also but that's on its way when the code itself looks good for the reviewers.
___
MaxScene Family #4615
Introduction of the Max Scene Family
___
Maya: Multiple values on single render attribute - OP-4131 #4631
When validating render attributes, this adds support for multiple values. When repairing first value in list is used.
___
Maya: enable 2D Pan/Zoom for playblasts - OP-5213 #4687
Setting for enabling 2D Pan/Zoom on reviews.
___
Copy existing or generate new Fusion profile on prelaunch #4572
Fusion preferences will be copied to the predefined `~/.openpype/hosts/fusion/prefs` folder (or any other folder set in system settings) on launch.
The idea is to create a copy of existing Fusion profile, adding an OpenPype menu to the Fusion instance.By default the copy setting is turned off, so no file copying is performed. Instead the clean Fusion profile is created by Fusion in the predefined folder. The default locaion is set to `~/.openpype/hosts/fusion/prefs`, to better comply with the other os platforms. After creating the default profile, some modifications are applied:
- forced Python3
- forced English interface
- setup Openpype specific path maps.If the `copy_prefs` checkbox is toggled, a copy of existing Fusion profile folder will be placed in the mentioned location. Then they are altered the same way as described above. The operation is run only once, on the first launch, unless the `force_sync [Resync profile on each launch]` is toggled.English interface is forced because the `FUSION16_PROFILE_DIR` environment variable is not read otherwise (seems to be a Fusion bug).
___
Houdini: Create button open new publisher's "create" tab #4601
During a talk with @maxpareschi he mentioned that the new publisher in Houdini felt super confusing due to "Create" going to the older creator but now being completely empty and the publish button directly went to the publish tab.This resolves that by fixing the Create button to now open the new publisher but on the Create tab.Also made publish button enforce going to the "publish" tab for consistency in usage.@antirotor I think changing the Create button's callback was just missed in this commit or was there a specific reason to not change that around yet?
___
Clockify: refresh and fix the integration #4607
Due to recent API changes, Clockify requires `user_id` to operate with the timers. I updated this part and currently it is a WIP for making it fully functional. Most functions, such as start and stop timer, and projects sync are currently working. For the rate limiting task new dependency is added: https://pypi.org/project/ratelimiter/
___
Fusion publish existing frames #4611
This PR adds the function to publish existing frames instead of having to re-render all of them for each new publish.I have split the render_locally plugin so the review-part is its own plugin now.I also change the saver-creator-plugin's label from Saver to Render (saver) as I intend to add a Prerender creator like in Nuke.
___
Resolution settings referenced from DB record for 3dsMax #4652
- Add Callback for setting the resolution according to DB after the new scene is created.
- Add a new Action into openpype menu which allows the user to reset the resolution in 3dsMax
___
3dsmax: render instance settings in Publish tab #4658
Allows user preset the pools, group and use_published settings in Render Creator in the Max Hosts.User can set the settings before or after creating instance in the new publisher
___
scene length setting referenced from DB record for 3dsMax #4665
Setting the timeline length based on DB record in 3dsMax Hosts
___
Publisher: Windows reduce command window pop-ups during Publishing #4672
Reduce the command line pop-ups that show on Windows during publishing.
___
Publisher: Explicit save #4676
Publisher have explicit button to save changes, so reset can happen without saving any changes. Save still happens automatically when publishing is started or on publisher window close. But a popup is shown if context of host has changed. Important context was enhanced by workfile path (if host integration supports it) so workfile changes are captured too. In that case a dialog with confirmation is shown to user. All callbacks that may require save of context were moved to main window to be able handle dialog show at one place. Save changes now returns success so the rest of logic is skipped -> publishing won't start, when save of instances fails.Save and reset buttons have shortcuts (Ctrl + s and Ctrls + r).
___
CelAction: conditional workfile parameters from settings #4677
Since some productions were requesting excluding some workfile parameters from publishing submission, we needed to move them to settings so those could be altered per project.
___
Improve logging of used app + tool envs on application launch #4682
Improve logging of what apps + tool environments got loaded for an application launch.
___
Fix name and docstring for Create Workdir Extra Folders prelaunch hook #4683
Fix class name and docstring for Create Workdir Extra Folders prelaunch hookThe class name and docstring were originally copied from another plug-in and didn't match the plug-in logic.This also fixes potentially seeing this twice in your logs. Before:After:Where it was actually running both this prelaunch hook and the actual `AddLastWorkfileToLaunchArgs` plugin.
___
Application launch context: Include app group name in logger #4684
Clarify in logs better what app group the ApplicationLaunchContext belongs to and what application is being launched.Before:After:
___
increment workfile version 3dsmax #4685
increment workfile version in 3dsmax as if in blender and maya hosts.
___
Maya: Fix getting non-active model panel. #2968
When capturing multiple cameras with image planes that have file sequences playing, only the active (first) camera will play through the file sequence.
___
Maya: Fix broken review publishing. #4549
Resolves #4547
___
Maya: Avoid error on right click in Loader if `mtoa` is not loaded #4616
Fix an error on right clicking in the Loader when `mtoa` is not a loaded plug-in.Additionally if `mtoa` isn't loaded the loader will now load the plug-in before trying to create the arnold standin.
___
Maya: Fix extract look colorspace detection #4618
Fix the logic which guesses the colorspace using `arnold` python library.
- Previously it'd error if `mtoa` was not available on path so it still required `mtoa` to be available.
- The guessing colorspace logic doesn't actually require `mtoa` to be loaded, but just the `arnold` python library to be available. This changes the logic so it doesn't require the `mtoa` plugin to get loaded to guess the colorspace.
- The if/else branch was likely not doing what was intended `cmds.loadPlugin("mtoa", quiet=True)` returns None if the plug-in was already loaded. So this would only ever be true if it ends up loading the `mtoa` plugin the first time.
```python
# Tested in Maya 2022.1
print(cmds.loadPlugin("mtoa", quiet=True))
# ['mtoa']
print(cmds.loadPlugin("mtoa", quiet=True))
# None
```
___
Maya: Maya Playblast Options overrides - OP-3847 #4634
When publishing a review in Maya, the extractor would fail due to wrong (long) panel name.
___
Bugfix/op 2834 fix extract playblast #4701
Paragraphs contain detailed information on the changes made to the product or service, providing an in-depth description of the updates and enhancements. They can be used to explain the reasoning behind the changes, or to highlight the importance of the new features. Paragraphs can often include links to further information or support documentation.
___
Bugfix/op 2834 fix extract playblast #4704
Paragraphs contain detailed information on the changes made to the product or service, providing an in-depth description of the updates and enhancements. They can be used to explain the reasoning behind the changes, or to highlight the importance of the new features. Paragraphs can often include links to further information or support documentation.
___
Maya: bug fix for passing zoom settings if review is attached to subset #4716
Fix for attaching review to subset with pan/zoom option.
___
Maya: tile assembly fail in draft - OP-4820 #4416
Tile assembly in Deadline was broken.
Initial bug report revealed other areas of the tile assembly that needed fixing.
___
Maya: Yeti Validate Rig Input - OP-3454 #4554
Fix Yeti Validate Rig Input
Existing workflow was broken due to this #3297.
___
Scene inventory: Fix code errors when "not found" entries are found #4594
Whenever a "NOT FOUND" entry is present a lot of errors happened in the Scene Inventory:
- It started spamming a lot of errors for the VersionDelegate since it had no numeric version (no version at all).Error reported on Discord:
```python
Traceback (most recent call last):
File "C:\Users\videopro\Documents\github\OpenPype\openpype\tools\utils\delegates.py", line 65, in paint
text = self.displayText(
File "C:\Users\videopro\Documents\github\OpenPype\openpype\tools\utils\delegates.py", line 33, in displayText
assert isinstance(value, numbers.Integral), (
AssertionError: Version is not integer. "None" Kitsu: Sync OP with zou, make sure value-data is int or float #4596
Currently the data zou pulls is a string and not a value causing some bugs in the pipe where a value is expected (like `Set frame range` in Fusion).
This PR makes sure each value is set with int() or float() so these bugs can't happen later on.
_(A request to cgwire has also bin sent to allow force values only for some metadata columns, but currently the user can enter what ever they want in there)_
___
Max: fix the bug of removing an instance #4617
fix the bug of removing an instance in 3dsMax
___
Global | Nuke: fixing farm publishing workflow #4623
After Nuke had adopted new publisher with new creators new issues were introduced. Those issues were addressed with this PR. Those are for example broken reviewable video files publishing if published via farm. Also fixed local publishing.
___
Ftrack: Ftrack additional families filtering #4633
Ftrack family collector makes sure the subset family is also in instance families for additional families filtering.
___
Ftrack: Hierarchical <> Non-Hierarchical attributes sync fix #4635
Sync between hierarchical and non-hierarchical attributes should be fixed and work as expected. Action should sync the values as expected and event handler should do it too and only on newly created entities.
___
bugfix for 3dsmax publishing error #4637
fix the bug of failing publishing job in 3dsMax
___
General: Use right validation for ffmpeg executable #4640
Use ffmpeg exec validation for ffmpeg executables instead of oiio exec validation. The validation is used as last possible source of ffmpeg from `PATH` environment variables, which is an edge case but can cause issues.
___
3dsmax: opening last workfile #4644
Supports opening last saved workfile in 3dsmax host.
___
Fixed a bug where a QThread in the splash screen could be destroyed before finishing execution #4647
This should fix the occasional behavior of the QThread being destroyed before even its worker returns from the `run()` function.After quiting, it should wait for the QThread object to properly close itself.
___
General: Use right plugin class for Collect Comment #4653
Collect Comment plugin is instance plugin so should inherit from `InstancePlugin` instead of `ContextPlugin`.
___
Global: add tags field to thumbnail representation #4660
Thumbnail representation might be missing tags field.
___
Integrator: Enforce unique destination transfers, disallow overwrites in queued transfers #4662
Fix #4656 by enforcing unique destination transfers in the Integrator. It's now disallowed to a destination in the file transaction queue with a new source path during the publish.
___
Hiero: Creator with correct workfile numeric padding input #4666
Creator was showing 99 in workfile input for long time, even if users set default value to 1001 in studio settings. This has been fixed now.
___
Nuke: Nukenodes family instance without frame range #4669
No need to add frame range data into `nukenodes` (backdrop) family publishes - since those are timeless.
___
TVPaint: Optional Validation plugins can be de/activated by user #4674
Added `OptionalPyblishPluginMixin` to TVpaint plugins that can be optional.
___
Kitsu: Slightly less strict with instance data #4678
- Allow to take task name from context if asset doesn't have any. Fixes an issue with Photoshop's review instance not having `task` in data.
- Allow to match "review" against both `instance.data["family"]` and `instance.data["families"]` because some instances don't have the primary family in families, e.g. in Photoshop and TVPaint.
- Do not error on Integrate Kitsu Review whenever for whatever reason Integrate Kitsu Note did not created a comment but just log the message that it was unable to connect a review.
___
Publisher: Fix reset shortcut sequence #4694
Fix bug created in https://github.com/ynput/OpenPype/pull/4676 where key sequence is checked using unsupported method. The check was changed to convert event into `QKeySequence` object which can be compared to prepared sequence.
___
Refactor _capture #4702
Paragraphs contain detailed information on the changes made to the product or service, providing an in-depth description of the updates and enhancements. They can be used to explain the reasoning behind the changes, or to highlight the importance of the new features. Paragraphs can often include links to further information or support documentation.
___
Hiero: correct container colors if UpToDate #4708
Colors on loaded containers are now correctly identifying real state of version. `Red` for out of date and `green` for up to date.
___
Look Assigner: Move Look Assigner tool since it's Maya only #4604
Fix #4357: Move Look Assigner tool to maya since it's Maya only
___
Maya: Remove unused functions from Extract Look #4671
Remove unused functions from Maya Extract Look plug-in
___
Extract Review code refactor #3930
Trying to reduce complexity of Extract Review plug-in
- Re-use profile filtering from lib
- Remove "combination families" additional filtering which supposedly was from OP v2
- Simplify 'formatting' for filling gaps
- Use `legacy_io.Session` over `os.environ`
___
Maya: Replace last usages of Qt module #4610
Replace last usage of `Qt` module with `qtpy`. This change is needed for `PySide6` support. All changes happened in Maya loader plugins.
___
Update tests and documentation for `ColormanagedPyblishPluginMixin` #4612
Refactor `ExtractorColormanaged` to `ColormanagedPyblishPluginMixin` in tests and documentation.
___
Improve logging of used app + tool envs on application launch (minor tweak) #4686
Use `app.full_name` for change done in #4682
___
Docs/add architecture document #4344
Add `ARCHITECTURE.md` document.
his document attemps to give a quick overview of the project to help onboarding, it's not an extensive documentation but more of a elevator pitch one-line descriptions of files/directories and what the attempt to do.
___
Documentation: Tweak grammar and fix some typos #4613
This resolves some grammar and typos in the documentation.Also fixes the extension of some images in after effects docs which used uppercase extension even though files were lowercase extension.
___
Docs: Fix some minor grammar/typos #4680
Typo/grammar fixes in documentation.
___
Maya: Implement image file node loader #4313
Implements a loader for loading texture image into a `file` node in Maya.
Similar to Maya's hypershade creation of textures on load you have the option to choose for three modes of creating:
- Texture
- Projection
- StencilThese should match what Maya generates if you create those in Maya.
- [x] Load and manage file nodes
- [x] Apply color spaces after #4195
- [x] Support for _either_ UDIM or image sequence - currently it seems to always load sequences as UDIM automatically.
- [ ] Add support for animation sequences of UDIM textures using the `Maya Look Assigner: Don't rely on containers for get all assets #4600
This resolves #4044 by not actually relying on containers in the scene but instead just rely on finding nodes with `cbId` attributes. As such, imported nodes would also be found and a shader can be assigned (similar to when using get from selection).**Please take into consideration the potential downsides below**Potential downsides would be:
- IF an already loaded look has any dagNodes, say a 3D Projection node - then that will also show up as a loaded asset where previously nodes from loaded looks were ignored.
- If any dag nodes were created locally - they would have gotten `cbId` attributes on scene save and thus the current asset would almost always show?
___
Maya: Unify menu labels for "Set Frame Range" and "Set Resolution" #4605
Fix #4109: Unify menu labels for "Set Frame Range" and "Set Resolution"This also tweaks it in Houdini from Reset Frame Range to Set Frame Range.
___
Resolve missing OPENPYPE_MONGO in deadline global job preload #4484
In the GlobalJobPreLoad plugin, we propose to replace the SpawnProcess by a sub-process and to pass the environment variables in the parameters, since the SpawnProcess under Centos Linux does not pass the environment variables.
In the GlobalJobPreLoad plugin, the Deadline SpawnProcess is used to start the OpenPype process. The problem is that the SpawnProcess does not pass environment variables, including OPENPYPE_MONGO, to the process when it is under Centos7 linux, and the process gets stuck. We propose to replace it by a subprocess and to pass the variable in the parameters.
___
Tests: Added setup_only to tests #4591
Allows to download test zip, unzip and restore DB in preparation for new test.
___
Maya: Arnold don't reset maya timeline frame range on render creation (or setting render settings) #4603
Fix #4429: Do not reset fps or playback timeline on applying or creating render settings
___
Bump @sideway/formula from 3.0.0 to 3.0.1 in /website #4609
Bumps [@sideway/formula](https://github.com/sideway/formula) from 3.0.0 to 3.0.1.
Commits
5b44c1b 3.0.19fbc20a chore: better number regex41ae98e Cleanupc59f35e Move to SidewayMaintainer changes
@sideway/formula since your current version.
[](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)
Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`.
[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)
---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually
- `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
- `@dependabot use these labels` will set the current labels as the default for future PRs for this repo and language
- `@dependabot use these reviewers` will set the current reviewers as the default for future PRs for this repo and language
- `@dependabot use these assignees` will set the current assignees as the default for future PRs for this repo and language
- `@dependabot use this milestone` will set the current milestone as the default for future PRs for this repo and language
You can disable automated security fix PRs for this repo from the [Security Alerts page](https://github.com/ynput/OpenPype/network/alerts).
Update artist_hosts_maya_arnold.md #4626
Correct Arnold docs.
___
Maya: Add "Include Parent Hierarchy" option in animation creator plugin #4645
Add an option in Project Settings > Maya > Creator Plugins > Create Animation to include (or not) parent hierarchy. This is to avoid artists to check manually the option for all create animation.
___
General: Filter available applications #4667
Added option to filter applications that don't have valid executable available in settings in launcher and ftrack actions. This option can be disabled in new settings category `Applications`. The filtering is by default disabled.
___
3dsmax: make sure that startup script executes #4695
Fixing reliability of OpenPype startup in 3dsmax.
___
Project Manager: Change minimum frame start/end to '0' #4719
Project manager can have frame start/end set to `0`.
___
maya gltf texture convertor and validator #4261
Continuity of the gltf extractor implementation
Continuity of the gltf extractor https://github.com/pypeclub/OpenPype/pull/4192UPDATE:**Validator for GLSL Shader**: Validate whether the mesh uses GLSL Shader. If not it will error out. The user can choose to perform the repair action and it will help to assign glsl shader. If the mesh with Stringray PBS, the repair action will also check to see if there is any linked texture such as Color, Occulsion, and Normal Map. If yes, it will help to relink the related textures to the glsl shader.*****If the mesh uses the PBS Shader,
___
Unreal: New Publisher #4370
Implementation of the new publisher for Unreal.
The implementation of the new publisher for Unreal. This PR includes the changes for all the existing creators to be compatible with the new publisher.The basic creator has been split in two distinct creators:
- `UnrealAssetCreator`, works with assets in the Content Browser.
- `UnrealActorCreator` that works with actors in the scene.
___
Implementation of a new splash screen #4592
Implemented a new splash screen widget to reflect a process running in the background. This widget can be used for other tasks than UE. **Also fixed the compilation error of the AssetContainer.cpp when trying to build the plugin in UE 5.0**
___
Deadline for 3dsMax #4439
Setting up deadline for 3dsmax
Setting up deadline for 3dsmax by setting render outputs and viewport camera
___
Nuke: adding nukeassist #4494
Adding support for NukeAssist
For support of NukeAssist we had to limit some Nuke features since NukeAssist itself Nuke with limitations. We do not support Creator and Publisher. User can only Load versions with version control. User can also set Framerange and Colorspace.
___
Maya: OP-2630 acescg maya #4340
Resolves #2712
___
Default Ftrack Family on RenderLayer #4458
With default settings, renderlayers in Maya were not being tagged with the Ftrack family leading to confusion when doing reviews.
___
Maya: Maya Playblast Options - OP-3783 #4487
Replacement PR for #3912. Adds more options for playblasts to preferences/settings.
Adds the following as options in generating playblasts, matching viewport settings.
- Use default material
- Wireframe on shaded
- X-ray
- X-ray Joints
- X-ray active component
___
Maya: Passing custom attributes to alembic - OP-4111 #4516
Passing custom attributes to alembic
This PR makes it possible to pass all user defined attributes along to the alembic representation.
___
Maya: Options for VrayProxy output - OP-2010 #4525
Options for output of VrayProxy.
Client requested more granular control of output from VrayProxy instance. Exposed options on the instance and settings for vrmesh and alembic.
___
Maya: Validate missing instance attributes #4559
Validate missing instance attributes.
New attributes can be introduced as new features come in. Old instances will need to be updated with these attributes for the documentation to make sense, and users do not have to recreate the instances.
___
Refactored Generation of UE Projects, installation of plugins moved to the engine #4369
Improved the way how OpenPype works with generation of UE projects. Also the installation of the plugin has been altered to install into the engine
OpenPype now uses the appropriate tools to generate UE projects. Unreal Build Tool (UBT) and a "Commandlet Project" is needed to properly generate a BP project, or C++ code in case that `dev_mode = True`, folders, the .uproject file and many other resources.On the plugin's side, it is built seperately with the UnrealAutomationTool (UAT) and then it's contents are moved under the `Engine/Plugins/Marketplace/OpenPype` directory.
___
Unreal: Use client functions in Layout loader #4578
Use 'get_representations' instead of 'legacy_io' query in layout loader.
This is removing usage of `find_one` called on `legacy_io` and use rather client functions as preparation for AYON connection. Also all representations are queried at once instead of one by one.
___
General: Support for extensions filtering in loaders #4492
Added extensions filtering support to loader plugins.
To avoid possible backwards compatibility break is filtering exactly the same and filtering by extensions is enabled only if class attribute 'extensions' is set.
___
Nuke: multiple reformat in baking review profiles #4514
Added support for multiple reformat nodes in baking profiles.
Old settings for single reformat node is supported and prioritised just in case studios are using it and backward compatibility is needed. Warnings in Nuke terminal are notifying users to switch settings to new workflow. Settings are also explaining the migration way.
___
Nuke: Add option to use new creating system in workfile template builder #4545
Nuke workfile template builder can use new creators instead of legacy creators.
Modified workfile template builder to have option to say if legacy creators should be used or new creators. Legacy creators are disabled by default, so Maya has changed the value.
___
Global, Nuke: Workfile first version with template processing #4579
Supporting new template workfile builder with toggle for creation of first version of workfile in case there is none yet.
___
Fusion: New Publisher #4523
This is an updated PR for @BigRoy 's old PR (https://github.com/ynput/OpenPype/pull/3892).I have merged it with code from OP 3.15.1-nightly.6 and made sure it works as expected.This converts the old publishing system to the new one. It implements Fusion as a new host addon.
- Create button removed in OpenPype menu in favor of the new Publisher
- Draft refactor validations to raise PublishValidationError
- Implement Creator for New Publisher
- Implement Fusion as Host addon
___
TVPaint: Use Publisher tool #4471
Use Publisher tool and new creation system in TVPaint integration.
Using new creation system makes TVPaint integration a little bit easier to maintain for artists. Removed unneeded tools Creator and Subset Manager tools. Goal is to keep the integration work as close as possible to previous integration. Some changes were made but primarilly because they were not right using previous system.All creators create instance with final family instead of changing the family during extraction. Render passes are not related to group id but to render layer instance. Render layer is still related to group. Workfile, review and scene render instances are created using autocreators instead of auto-collection during publishing. Subset names are fully filled during publishing but instance labels are filled on refresh with the last known right value. Implemented basic of legacy convertor which should convert render layers and render passes.
___
TVPaint: Auto-detect render creation #4496
Create plugin which will create Render Layer and Render Pass instances based on information in the scene.
Added new creator that must be triggered by artist. The create plugin will first create Render Layer instances if were not created yet. For variant is used color group name. The creator has option to rename color groups by template defined in settings -> Template may use index of group by it's usage in scene (from bottom to top). After Render Layers will create Render Passes. Render Pass is created for each individual TVPaint layer in any group that had created Render Layer. It's name is used as variant (pass).
___
TVPaint: Small enhancements #4501
Small enhancements in TVPaint integration which did not get to https://github.com/ynput/OpenPype/pull/4471.
It was found out that `opacity` returned from `tv_layerinfo` is always empty and is dangerous to add it to layer information. Added information about "current" layer to layers information. Disable review of Render Layer and Render Pass instances by default. In most of productions is used only "scene review". Skip usage of `"enabled"` key from settings in automated layer/pass creation.
___
Global: color v3 global oiio transcoder plugin #4291
Implements possibility to use `oiiotool` to transcode image sequences from one color space to another(s).
Uses collected `colorspaceData` information about source color spaces, these information needs to be collected previously in each DCC interested in color management.Uses profiles configured in Settings to create single or multiple new representations (and file extensions) with different color spaces.New representations might replace existing one, each new representation might contain different tags and custom tags to control its integration step.
___
Deadline: Added support for multiple install dirs in Deadline #4451
SearchDirectoryList returns FIRST existing so if you would have multiple OP install dirs, it won't search for appropriate version in later ones.
___
Ftrack: Upload reviewables with original name #4483
Ftrack can integrate reviewables with original filenames.
As ftrack have restrictions about names of components the only way how to achieve the result was to upload the same file twice, one with required name and one with origin name.
___
TVPaint: Ignore transparency in Render Pass #4499
It is possible to ignore layers transparency during Render Pass extraction.
Render pass extraction does not respect opacity of TVPaint layers set in scene during extraction. It can be enabled/disabled in settings.
___
Anatomy: Preparation for different root overrides #4521
Prepare Anatomy to handle only 'studio' site override on it's own.
Change how Anatomy fill root overrides based on requested site name. The logic which decide what is active site was moved to sync server addon and the same for receiving root overrides of local site. The Anatomy resolve only studio site overrides anything else is handled by sync server. BaseAnatomy only expect root overrides value and does not need site name. Validation of site name happens in sync server same as resolving if site name is local or not.
___
Nuke | Global: colormanaged plugin in collection #4556
Colormanaged extractor had changed to Mixin class so it can be added to any stage of publishing rather then just to Exctracting.Nuke is no collecting colorspaceData to representation collected on already rendered images.
Mixin class can no be used as secondary parent in publishing plugins.
___
look publishing and srgb colorspace in maya #4276
Check the OCIO color management is enabled before doing linearize colorspace for converting the texture maps into tx files.
Check whether the OCIO color management is enabled before the condition of converting the texture to tx extension.
___
Maya: extract Thumbnail "No active model panel found" - OP-3849 #4421
Error when extracting playblast with no model panel.
If `project_settings/maya/publish/ExtractPlayblast/capture_preset/Viewport Options/override_viewport_options` were off and publishing without showing any model panel, the extraction would fail.
___
Maya: Fix setting scene fps with float input #4488
Returned value of float fps on integer values would return float.
This PR fixes the case when switching between integer fps values for example 24 > 25. Issue was when setting the scene fps, the original float value was used which makes it unpredictable whether the value is float or integer when mapping the fps values.
___
Maya: Multipart fix #4497
Fix multipart logic in render products.
Each renderer has a different way of defining whether output images is multipart, so we need to define it for each renderer. Also before the `multipart` class variable was defined multiple times in several places, which made it tricky to debug where `multipart` was defined. Now its created on initialization and referenced as `self.multipart`
___
Maya: Set pool on tile assembly - OP-2012 #4520
Set pool on tile assembly
Pool for publishing and tiling jobs, need to use the settings (`project_settings/deadline/publish/ProcessSubmittedJobOnFarm/deadline_pool`) else fallback on primary pool (`project_settings/deadline/publish/CollectDeadlinePools/primary_pool`)
___
Maya: Extract review with handles #4527
Review was not extracting properly with/without handles.
Review instance was not created properly resulting in the frame range on the instance including handles.
___
Maya: Fix broken lib. #4529
Fix broken lib.
This commit from this PR broke the Maya lib module.
___
Maya: Validate model name - OP-4983 #4539
Validate model name issues.
Couple of issues with validate model name;
- missing platform extraction from settings
- map function should be list comprehension
- code cosmetics
___
Maya: SkeletalMesh family loadable as reference #4573
In Maya, fix the SkeletalMesh family not loadable as reference.
___
Unreal: fix loaders because of missing AssetContainer #4536
Fixing Unreal loaders, where changes in OpenPype Unreal integration plugin deleted AssetContainer.
`AssetContainer` and `AssetContainerFactory` are still used to mark loaded instances. Because of optimizations in Integration plugin we've accidentally removed them but that broke loader.
___
3dsmax unable to delete loaded asset in the scene inventory #4507
Fix the bug of being unable to delete loaded asset in the Scene Inventory
Fix the bug of being unable to delete loaded asset in the Scene Inventory
___
Hiero/Nuke: originalBasename editorial publishing and loading #4453
Publishing and loading `originalBasename` is working as expected
Frame-ranges on version document is now correctly defined to fit original media frame range which is published. It means loading is now correctly identifying frame start and end on clip loader in Nuke.
___
Nuke: Fix workfile template placeholder creation #4512
Template placeholder creation was erroring out in Nuke due to the Workfile template builder not being able to find any of the plugins for the Nuke host.
Move `get_workfile_build_placeholder_plugins` function to NukeHost class as workfile template builder expects.
___
Nuke: creator farm attributes from deadline submit plugin settings #4519
Defaults in farm attributes are sourced from settings.
Settings for deadline nuke submitter are now used during nuke render and prerender creator plugins.
___
Nuke: fix clip sequence loading #4574
Nuke is loading correctly clip from image sequence created without "{originalBasename}" token in anatomy template.
___
Fusion: Fix files collection and small bug-fixes #4423
Fixed Fusion review-representation and small bug-fixes
This fixes the problem with review-file generation that stopped the publishing on second publish before the fix.The problem was that Fusion simply looked at all the files in the render-folder instead of only gathering the needed frames for the review.Also includes a fix to get the handle start/end that before throw an error if the data didn't exist (like from a kitsu sync).
___
Fusion: Updated render_local.py to not only process the first instance #4522
Moved the `__hasRun` to `render_once()` so the check only happens with the rendering. Currently only the first render node gets the representations added.Critical PR
___
Fusion: Load sequence fix filepath resolving from representation #4580
Resolves issue mentioned on discord by @movalex:The loader was incorrectly trying to find the file in the publish folder which resulted in just picking 'any first file'.
This gets the filepath from representation instead of taking the first file from listing files from publish folder.
___
Fusion: Fix review burnin start and end frame #4590
Fix the burnin start and end frame for reviews. Without this the asset document's start and end handle would've been added to the _burnin_ frame range even though that would've been incorrect since the handles are based on the comp saver's render range instead.
___
Harmony: missing set of frame range when opening scene #4485
Frame range gets set from DB everytime scene is opened.
Added also check for not up-to-date loaded containers.
___
Photoshop: context is not changed in publisher #4570
When PS is already open and artists launch new task, it should keep only opened PS open, but change context.
Problem were occurring in Workfile app where under new task files from old task were shown. This fixes this and adds opening of last workfile for new context if workfile exists.
___
hiero: fix effect item node class #4543
Collected effect name after renaming is saving correct class name.
___
Bugfix/OP-4616 vray multipart #4297
This fixes a bug where multipart vray renders would not make a review in Ftrack.
___
Maya: Fix changed location of reset_frame_range #4491
Location in commands caused cyclic import
___
global: source template fixed frame duplication #4503
Duplication is not happening.
Template is using `originalBasename` which already assume all necessary elements are part of the file name so there was no need for additional optional name elements.
___
Deadline: Hint to use Python 3 #4518
Added shebank to give deadline hint which python should be used.
Deadline has issues with Python 2 (especially with `os.scandir`). When a shebank is added to file header deadline will use python 3 mode instead of python 2 which fix the issue.
___
Publisher: Prevent access to create tab after publish start #4528
Prevent access to create tab after publish start.
Disable create button in instance view on publish start and enable it again on reset. Even with that make sure that it is not possible to go to create tab if the tab is disabled.
___
Color Transcoding: store target_colorspace as new colorspace #4544
When transcoding into new colorspace, representation must carry this information instead original color space.
___
Deadline: fix submit_publish_job #4552
Fix submit_publish_job
Resolves #4541
___
Kitsu: Fix task itteration in update-op-with-zou #4577
From the last PR (https://github.com/ynput/OpenPype/pull/4425) a comment-commit last second messed up the code and resulted in two lines being the same, crashing the script. This PR fixes that.
___
AttrDefs: Fix type for PySide6 #4584
Use right type in signal emit for value change of attribute definitions.
Changed `UUID` type to `str`. This is not an issue with PySide2 but it is with PySide6.
___
Scene Inventory: Avoid using ObjectId #4524
Avoid using conversion to ObjectId type in scene inventory tool.
Preparation for AYON compatibility where ObjectId won't be used for ids. Representation ids from loaded containers are not converted to ObjectId but kept as strings which also required some changes when working with representation documents.
___
SiteSync: host dirmap is not working properly #4563
If artists uses SiteSync with real remote (gdrive, dropbox, sftp) drive, Local Settings were throwing error `string indices must be integers`.
Logic was reworked to provide only `local_drive` values to be overrriden by Local Settings. If remote site is `gdrive` etc. mapping to `studio` is provided as it is expected that workfiles will have imported from `studio` location and not from `gdrive` folder.Also Nuke dirmap was reworked to be less verbose and much faster.
___
General: Input representation ids are not ObjectIds #4576
Don't use `ObjectId` as representation ids during publishing.
Representation ids are kept as strings during publishing instead of converting them to `ObjectId`. This change is pre-requirement for AYON connection.Inputs are used for integration of links and for farm publishing (or at least it looks like).
___
Shotgrid: Fixes on Deadline submissions #4498
A few other bug fixes for getting Nuke submission to Deadline work smoothly using Shotgrid integration.
Continuing on the work done on this other PR this fixes a few other bugs I came across with further tests.
___
Fusion: New Publisher #3892
This converts the old publishing system to the new one. It implements Fusion as a new host addon.
- Create button removed in OpenPype menu in favor of the new Publisher
- Draft refactor validations to raise `PublishValidationError`
- Implement Creator for New Publisher
- Implement Fusion as Host addon
___
Make Kitsu work with Tray Publisher, added kitsureview tag, fixed sync-problems. #4425
Make Kitsu work with Tray Publisher, added kitsureview tag, fixed sync-problems.
This PR updates the way the module gather info for the current publish so it now works with Tray Publisher.It fixes the data that gets synced from Kitsu to OP so all needed data gets registered even if it doesn't exist on Kitsus side.It also adds the tag "Add review to Kitsu" and adds it to Burn In so previews gets generated by default to Kitsu.
___
Maya: V-Ray Set Image Format from settings #4566
Resolves #4565
Set V-Ray Image Format using settings.
___
Maya: Xgen (3d / maya ) - #4256
___
#### Brief description
Initial Xgen implementation.
#### Description
Client request of Xgen pipeline.
___
Data exchange cameras for 3d Studio Max (3d / 3dsmax ) - #4376
___
#### Brief description
Add Camera Family into the 3d Studio Max
#### Description
Adding Camera Extractors(extract abc camera and extract fbx camera) and validators(for camera contents) into 3dMaxAlso add the extractor for exporting 3d max raw scene (which is also related to 3dMax Scene Family) for camera family
___
Adding path validator for non-maya nodes (3d / maya ) - #4271
___
#### Brief description
Adding a path validator for filepaths from non-maya nodes, which are created by plugins such as Renderman, Yeti and abcImport.
#### Description
As File Path Editor cannot catch the wrong filenpaths from non-maya nodes such as AlembicNodes, It is neccessary to have a new validator to ensure the existence of the filepaths from the nodes.
___
Deadline: Allow disabling strict error check in Maya submissions (3d / maya / deadline ) - #4420
___
#### Brief description
DL by default has Strict error checking, but some errors are not fatal.
#### Description
This allows to set profile based on Task and Subset values to temporarily disable Strict Error Checks.Subset and task names should support regular expressions. (not wildcard notation though).
___
Houdini: New publisher code tweak (3d / houdini ) - #4374
___
#### Brief description
This is cosmetics only - the previous code to me felt quite unreadable due to the lengthy strings being used.
#### Description
Code should do roughly the same, but just be reformatted.
___
3dsmax: enhance alembic loader update function (3d / 3dsmax ) - #4387
___
## Enhancement
This PR is adding update/switch ability to pointcache/alembic loader in 3dsmax and fixing wrong tool shown when clicking on "Manage" item on OpenPype menu, that is now correctly Scene Inventory (but was Subset Manager).
Alembic update has still one caveat - it doesn't cope with changed number of object inside alembic, since loading alembic in max involves creating all those objects as first class nodes. So it will keep the objects in scene, just update path to alembic file on them.
___
Global: supporting `OPENPYPE_TMPDIR` in staging dir maker (editorial / hiero ) - #4398
___
#### Brief description
Productions can use OPENPYPE_TMPDIR for staging temp publishing directory
#### Description
Studios were demanding to be able to configure their own shared storages as temporary staging directories. Template formatting is also supported with optional keys formatting and following anatomy keys: - root[work | General: Functions for current context (other ) - #4324
___
#### Brief description
Defined more functions to receive current context information and added the methods to host integration so host can affect the result.
#### Description
This is one of steps to reduce usage of `legacy_io.Session`. This change define how to receive current context information -> call functions instead of accessing `legacy_io.Session` or `os.environ` directly. Plus, direct access on session or environments is unfortunatelly not enough for some DCCs where multiple workfiles can be opened at one time which can heavily affect the context but host integration sometimes can't affect that at all.`HostBase` already had implemented `get_current_context`, that was enhanced by adding more specific methods `get_current_project_name`, `get_current_asset_name` and `get_current_task_name`. The same functions were added to `~/openpype/pipeline/cotext_tools.py`. The functions in context tools are calling host integration methods (if are available) otherwise are using environent variables as default implementation does. Also was added `get_current_host_name` to receive host name from registered host if is available or from environment variable.
___
Houdini: Do not visualize the hidden OpenPypeContext node (other / houdini ) - #4382
___
#### Brief description
Using the new publisher UI would generate a visible 'null' locator at the origin. It's confusing to the user since it's supposed to be 'hidden'.
#### Description
Before this PR the user would see a locator/null at the origin which was the 'hidden' `/obj/OpenPypeContext` node. This null would suddenly appear if the user would've ever opened the Publisher UI once.After this PR it will not show:Nice and tidy.
___
Maya + Blender: Pyblish plugins removed unused `version` and `category` attributes (other ) - #4402
___
#### Brief description
Once upon a time in a land far far away there lived a few plug-ins who felt like they didn't belong in generic boxes and felt they needed to be versioned well above others. They tried, but with no success.
#### Description
Even though they now lived in a universe with elaborate `version` and `category` attributes embedded into their tiny little plug-in DNA this particular deviation has been greatly unused. There is nothing special about the version, nothing special about the category.It does nothing.
___
General: Fix original basename frame issues (other ) - #4452
___
#### Brief description
Treat `{originalBasename}` in different way then standard files processing. In case template should use `{originalBasename}` the transfers will use them as they are without any changes or handling of frames.
#### Description
Frames handling is problematic with original basename because their padding can't be defined to match padding in source filenames. Also it limits the usage of functionality to "must have frame at end of fiename". This is proposal how that could be solved by simply ignoring frame handling and using filenames as are on representation. First frame is still stored to representation context but is not used in formatting part. This way we don't have to care about padding of frames at all.
___
Publisher: Report also crashed creators and convertors (other ) - #4473
___
#### Brief description
Added crashes of creators and convertos discovery (lazy solution).
#### Description
Report in Publisher also contains information about crashed files caused during creator plugin discovery and convertor plugin discovery. They're not separated into categroies and there is no other information in the report about them, but this helps a lot during development. This change does not need to change format/schema of the report nor UI logic.
___
Maya: Fix Validate Attributes plugin (3d / maya ) - #4401
___
#### Brief description
Code was broken. So either plug-in was unused or it had gone unnoticed.
#### Description
Looking at the commit history of the plug-in itself it seems this might have been broken somewhere between two to three years. I think it's broken since two years since this commit.Should this plug-in be removed completely?@tokejepsen Is there still a use case where we should have this plug-in? (You created the original one)
___
Maya: Ignore workfile lock in Untitled scene (3d / maya ) - #4414
___
#### Brief description
Skip workfile lock check if current scene is 'Untitled'.
___
Maya: fps rounding - OP-2549 (3d / maya ) - #4424
___
#### Brief description
When FPS is registered in for example Ftrack and round either down or up (floor/ceil), comparing to Maya FPS can fail. Example:23.97 (Ftrack/Mongo) != 23.976023976023978 (Maya)
#### Description
Since Maya only has a select number of supported framerates, I've taken the approach of converting any fps to supported framerates in Maya. We validate the input fps to make sure they are supported in Maya in two ways:Whole Numbers - are validated straight against the supported framerates in Maya.Demical Numbers - we find the closest supported framerate in Maya. If the difference to the closest supported framerate, is more than 0.5 we'll throw an error.If Maya ever supports arbitrary framerates, then we might have a problem but I'm not holding my breath...
___
Strict Error Checking Default (3d / maya ) - #4457
___
#### Brief description
Provide default of strict error checking for instances created prior to PR.
___
Create: Enhance instance & context changes (3d / houdini,after effects,3dsmax ) - #4375
___
#### Brief description
Changes of instances and context have complex, hard to get structure. The structure did not change but instead of complex dictionaries are used objected data.
#### Description
This is poposal of changes data improvement for creators. Implemented `TrackChangesItem` which handles the changes for us. The item is creating changes based on old and new value and can provide information about changed keys or access to full old or new value. Can give the values on any "sub-dictionary".Used this new approach to fix change in houdini and 3ds max and also modified one aftereffects plugin using changes.
___
Houdini: hotfix condition (3d / houdini ) - #4391
___
## Hotfix
This is fixing bug introduced int #4374
___
Houdini: Houdini shelf tools fixes (3d / houdini ) - #4428
___
#### Brief description
Fix Houdini shelf tools.
#### Description
Use `label` as mandatory key instead of `name`. Changed how shelves are created. If the script is empty it is gracefully skipping it instead of crashing.
___
3dsmax: startup fixes (3d / 3dsmax ) - #4412
___
#### Brief description
This is fixing various issues that can occur on some of the 3dsmax versions.
#### Description
On displays with +4K resolution UI was broken, some 3dsmax versions couldn't process `PYTHONPATH` correctly. This PR is forcing `sys.path` and disabling `QT_AUTO_SCREEN_SCALE_FACTOR`
___
Fix features for gizmo menu (2d / nuke ) - #4280
___
#### Brief description
Fix features for the Gizmo Menu project settings (shortcut for python type of usage and file type of usage functionality)
___
Photoshop: fix missing legacy io for legacy instances (2d / photoshop,after effects ) - #4467
___
#### Brief description
`legacy_io` import was removed, but usage stayed.
#### Description
Usage of `legacy_io` should be eradicated, in creators it should be replaced by `self.create_context.get_current_project_name/asset_name/task_name`.
___
Fix - addSite loader handles hero version (other / sitesync ) - #4359
___
#### Brief description
If adding site to representation presence of hero version is checked, if found hero version is marked to be donwloaded too.Replacing https://github.com/ynput/OpenPype/pull/4191
___
Remove OIIO build for macos (other ) - #4381
___
## Fix
Since we are not able to provide OpenImageIO tools binaries for macos, we should remove the item from th `pyproject.toml`. This PR is taking care of it.
It is also changing the way `fetch_thirdparty_libs` script works in that it doesn't crash when lib cannot be processed, it only issue warning.
Resolves #3858
___
General: Attribute definitions fixes (other ) - #4392
___
#### Brief description
Fix possible issues with attribute definitions in publisher if there is unknown attribute on an instance.
#### Description
Source of the issue is that attribute definitions from creator plugin could be "expanded" during `CreatedInstance` initialization. Which would affect all other instances using the same list of attributes -> literally object of list. If the same list object is used in "BaseClass" for other creators it would affect all instances (because of 1 instance). There had to be implemented other changes to fix the issue and keep behavior the same.Object of `CreatedInstance` can be created without reference to creator object. `CreatedInstance` is responsible to give UI attribute definitions (technically is prepared for cases when each instance may have different attribute definitions -> not yet).Attribute definition has added more conditions for `__eq__` method and have implemented `__ne__` method (which is required for Py 2 compatibility). Renamed `AbtractAttrDef` to `AbstractAttrDef` (fix typo).
___
Ftrack: Don't force ftrackapp endpoint (other / ftrack ) - #4411
___
#### Brief description
Auto-fill of ftrack url don't break custom urls. Custom urls couldn't be used as `ftrackapp.com` is added if is not in the url.
#### Description
The code was changed in a way that auto-fill is still supported but before `ftrackapp` is added it will try to use url as is. If the connection works as is it is used.
___
Fix: DL on MacOS (other ) - #4418
___
#### Brief description
This works if DL Openpype plugin Installation Directories is set to level of app bundle (eg. '/Applications/OpenPype 3.15.0.app')
___
Photoshop: make usage of layer name in subset name more controllable (other ) - #4432
___
#### Brief description
Layer name was previously used in subset name only if multiple instances were being created in single step. This adds explicit toggle.
#### Description
Toggling this button allows to use layer name in created subset name even if single instance is being created.This follows more closely implementation if AE.
___
SiteSync: fix dirmap (other ) - #4436
___
#### Brief description
Fixed issue in dirmap in Maya and Nuke
#### Description
Loads of error were thrown in Nuke console about dictionary value.`AttributeError: 'dict' object has no attribute 'lower'`
___
General: Ignore decode error of stdout/stderr in run_subprocess (other ) - #4446
___
#### Brief description
Ignore decode errors and replace invalid character (byte) with escaped byte character.
#### Description
Calling of `run_subprocess` may cause crashes if output contains some unicode character which (for example Polish name of encoder handler).
___
Publisher: Fix reopen bug (other ) - #4463
___
#### Brief description
Use right name of constant 'ActiveWindow' -> 'WindowActive'.
___
Publisher: Fix compatibility of QAction in Publisher (other ) - #4474
___
#### Brief description
Fix `QAction` for older version of Qt bindings where QAction requires a parent on initialization.
#### Description
This bug was discovered in Nuke 11. Fixed by creating QAction when QMenu is already available and can be used as parent.
___
General: Remove 'openpype.api' (other ) - #4413
___
#### Brief description
PR is removing `openpype/api.py` file which is causing a lot of troubles and cross-imports.
#### Description
I wanted to remove the file slowly function by function but it always reappear somewhere in codebase even if most of the functionality imported from there is triggering deprecation warnings. This is small change which may have huge impact.There shouldn't be anything in openpype codebase which is using `openpype.api` anymore so only possible issues are in customized repositories or custom addons.
___
docs-user-Getting Started adjustments (other ) - #4365
___
#### Brief description
Small typo fixes here and there, additional info on install/ running OP.
___
Renderman support for sample and display filters (3d / maya ) - #4003
___
#### Brief description
User can set up both sample and display filters in Openpype settings if they are using Renderman as renderer.
#### Description
You can preset which sample and display filters for renderman , including the cryptomatte renderpass, in Openpype settings. Once you select which filters to be included in openpype settings and then create render instance for your camera in maya, it would automatically tell the system to generate your selected filters in render settings.The place you can find for setting up the filters: _Maya > Render Settings > Renderman Renderer > Display Filters/ Sample Filters_
___
Maya: Create Arnold options on repair. (3d / maya ) - #4448
___
#### Brief description
When validating/repairing we previously required users to open render settings to create the Arnold options. This is done through code now.
___
Update Asset field of creator Instances in Maya Template Builder (3d / maya ) - #4470
___
#### Brief description
When we build a template with Maya Template Builder, it will update the asset field of the sets (creator instances) that are imported from the template.
#### Description
When building a template, we also want to define the publishable content in advance: create an instance of a model, or look, etc., to speed up the workflow and reduce the number of questions we are asked. After building a work file from a saved template that contains pre-created instances, the template builder should update the asset field to the current asset.
___
Blender: fix import workfile all families (3d / blender ) - #4405
___
#### Brief description
Having this feature related to workfile available for any family is absurd.
___
Nuke: update rendered frames in latest version (2d / nuke ) - #4362
___
#### Brief description
Introduced new field to insert frame(s) to rerender only.
#### Description
Rendering is expensive, sometimes it is helpful only to re-render changed frames and reuse existing.Artists can in Publisher fill which frame(s) should be re-rendered.If there is already published version of currently publishing subset, all representation files are collected (currently for `render` family only) and then when Nuke is rendering (locally only for now), old published files are copied into into temporary render folder where will be rewritten only by frames explicitly set in new field.That way review/burnin process could also reuse old files and recreate reviews/burnins.New version is produced during this process!
___
Feature: Keep synced hero representations up-to-date. (other ) - #4343
___
#### Brief description
Keep previously synchronized sites up-to-date by comparing old and new sites and adding old sites if missing in new ones.Fix #4331
___
Maya: Fix template builder bug where assets are not put in the right hierarchy (other ) - #4367
___
#### Brief description
When buiding scene from template, the assets loaded from the placeholders are not put in the hierarchy. Plus, the assets are loaded in double.
___
Bump ua-parser-js from 0.7.31 to 0.7.33 in /website (other ) - #4371
___
Bumps [ua-parser-js](https://github.com/faisalman/ua-parser-js) from 0.7.31 to 0.7.33.
Changelog
Version 0.7.31 / 1.0.2
Version 0.7.32 / 1.0.32
Version 0.7.33 / 1.0.33
Version 0.8
Commits
f2d0db0 Bump version 0.7.33a6140a1 Remove unsafe regex in trim() functiona886604 Fix #605 - Identify Macintosh as Apple deviceb814bcd Merge pull request #606 from rileyjshaw/patch-17f71024 Fix documentationc239ac5 Merge pull request #604 from obecerra3/master8d3c2d3 Add new browser: Cobaltd11fc47 Bump version 0.7.32b490110 Merge branch 'develop' of github.com:faisalman/ua-parser-jscb5da5e Merge pull request #600 from moekm/develop
[](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)
Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`.
[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)
---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually
- `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
- `@dependabot use these labels` will set the current labels as the default for future PRs for this repo and language
- `@dependabot use these reviewers` will set the current reviewers as the default for future PRs for this repo and language
- `@dependabot use these assignees` will set the current assignees as the default for future PRs for this repo and language
- `@dependabot use this milestone` will set the current milestone as the default for future PRs for this repo and language
You can disable automated security fix PRs for this repo from the [Security Alerts page](https://github.com/ynput/OpenPype/network/alerts).
Docs: Question about renaming in Kitsu (other ) - #4384
___
#### Brief description
To keep memory of this discussion: https://discord.com/channels/517362899170230292/563751989075378201/1068112668491255818
___
New Publisher: Fix Creator error typo (other ) - #4396
___
#### Brief description
Fixes typo in error message.
___
Chore: pyproject.toml version because of Poetry (other ) - #4408
___
#### Brief description
Automatization injects wrong format
___
Fix - remove minor part in toml (other ) - #4437
___
#### Brief description
Causes issue in create_env and new Poetry
___
General: Add project code to anatomy (other ) - #4445
___
#### Brief description
Added attribute `project_code` to `Anatomy` object.
#### Description
Anatomy already have access to almost all attributes from project anatomy except project code. This PR changing it. Technically `Anatomy` is everything what would be needed to get fill data of project.
```
{
"project": {
"name": anatomy.project_name,
"code": anatomy.project_code
}
}
```
___
Maya: Arnold Scene Source overhaul - OP-4865 (other / maya ) - #4449
___
#### Brief description
General overhaul of the Arnold Scene Source (ASS) workflow.
#### Description
This originally was to support static files (non-sequencial) ASS publishing, but digging deeper whole workflow needed an update to get ready for further issues. During this overhaul the following changes were made:
- Generalized Arnold Standin workflow to a single loader.
- Support multiple nodes as proxies.
- Support proxies for `pointcache` family.
- Generalized approach to proxies as resources, so they can be the same file format as the original.This workflow should allow further expansion to utilize operators and eventually USD.
___
Details for Ubuntu
Install git, cmake and curl
```sh
sudo apt install build-essential checkinstall
sudo apt install git cmake curl
```
#### Note:
In case you run in error about `xcb` when running OpenPype,
you'll need also additional libraries for Qt5:
```sh
sudo apt install qt5-default
```
or if you are on Ubuntu > 20.04, there is no `qt5-default` packages so you need to install its content individually:
```sh
sudo apt-get install qtbase5-dev qtchooser qt5-qmake qtbase5-dev-tools
```
Details for Centos
Install git, cmake and curl
```sh
sudo yum install qit cmake
```
#### Note:
In case you run in error about `xcb` when running OpenPype,
you'll need also additional libraries for Qt5:
```sh
sudo yum install qt5-qtbase-devel
```
Use pyenv to install Python version for OpenPype build
You will need **bzip2**, **readline**, **sqlite3** and other libraries.
For more details about Python build environments see:
https://github.com/pyenv/pyenv/wiki#suggested-build-environment
**For Ubuntu:**
```sh
sudo apt-get update; sudo apt-get install --no-install-recommends make build-essential libssl-dev zlib1g-dev libbz2-dev libreadline-dev libsqlite3-dev wget curl llvm libncurses5-dev xz-utils tk-dev libxml2-dev libxmlsec1-dev libffi-dev liblzma-dev
```
**For Centos:**
```sh
yum install gcc zlib-devel bzip2 bzip2-devel readline-devel sqlite sqlite-devel openssl-devel tk-devel libffi-devel
```
**install pyenv**
```sh
curl https://pyenv.run | bash
# you can add those to ~/.bashrc
export PATH="$HOME/.pyenv/bin:$PATH"
eval "$(pyenv init -)"
eval "$(pyenv virtualenv-init -)"
# reload shell
exec $SHELL
# install Python 3.9.x
pyenv install -v 3.9.6
# change path to OpenPype 3
cd /path/to/openpype-3
# set local python version
pyenv local 3.9.6
```
Show Value
.. code-block:: python
"""{{ obj.value|indent(width=8,blank=true) }}"""
.. raw:: html
*
*
* @return A new \c CSInterface object
*/
function CSInterface()
{
}
/**
* User can add this event listener to handle native application theme color changes.
* Callback function gives extensions ability to fine-tune their theme color after the
* global theme color has been changed.
* The callback function should be like below:
*
* @example
* // event is a CSEvent object, but user can ignore it.
* function OnAppThemeColorChanged(event)
* {
* // Should get a latest HostEnvironment object from application.
* var skinInfo = JSON.parse(window.__adobe_cep__.getHostEnvironment()).appSkinInfo;
* // Gets the style information such as color info from the skinInfo,
* // and redraw all UI controls of your extension according to the style info.
* }
*/
CSInterface.THEME_COLOR_CHANGED_EVENT = "com.adobe.csxs.events.ThemeColorChanged";
/** The host environment data object. */
CSInterface.prototype.hostEnvironment = window.__adobe_cep__ ? JSON.parse(window.__adobe_cep__.getHostEnvironment()) : null;
/** Retrieves information about the host environment in which the
* extension is currently running.
*
* @return A \c #HostEnvironment object.
*/
CSInterface.prototype.getHostEnvironment = function()
{
this.hostEnvironment = JSON.parse(window.__adobe_cep__.getHostEnvironment());
return this.hostEnvironment;
};
/** Closes this extension. */
CSInterface.prototype.closeExtension = function()
{
window.__adobe_cep__.closeExtension();
};
/**
* Retrieves a path for which a constant is defined in the system.
*
* @param pathType The path-type constant defined in \c #SystemPath ,
*
* @return The platform-specific system path string.
*/
CSInterface.prototype.getSystemPath = function(pathType)
{
var path = decodeURI(window.__adobe_cep__.getSystemPath(pathType));
var OSVersion = this.getOSInformation();
if (OSVersion.indexOf("Windows") >= 0)
{
path = path.replace("file:///", "");
}
else if (OSVersion.indexOf("Mac") >= 0)
{
path = path.replace("file://", "");
}
return path;
};
/**
* Evaluates a JavaScript script, which can use the JavaScript DOM
* of the host application.
*
* @param script The JavaScript script.
* @param callback Optional. A callback function that receives the result of execution.
* If execution fails, the callback function receives the error message \c EvalScript_ErrMessage.
*/
CSInterface.prototype.evalScript = function(script, callback)
{
if(callback === null || callback === undefined)
{
callback = function(result){};
}
window.__adobe_cep__.evalScript(script, callback);
};
/**
* Retrieves the unique identifier of the application.
* in which the extension is currently running.
*
* @return The unique ID string.
*/
CSInterface.prototype.getApplicationID = function()
{
var appId = this.hostEnvironment.appId;
return appId;
};
/**
* Retrieves host capability information for the application
* in which the extension is currently running.
*
* @return A \c #HostCapabilities object.
*/
CSInterface.prototype.getHostCapabilities = function()
{
var hostCapabilities = JSON.parse(window.__adobe_cep__.getHostCapabilities() );
return hostCapabilities;
};
/**
* Triggers a CEP event programmatically. Yoy can use it to dispatch
* an event of a predefined type, or of a type you have defined.
*
* @param event A \c CSEvent object.
*/
CSInterface.prototype.dispatchEvent = function(event)
{
if (typeof event.data == "object")
{
event.data = JSON.stringify(event.data);
}
window.__adobe_cep__.dispatchEvent(event);
};
/**
* Registers an interest in a CEP event of a particular type, and
* assigns an event handler.
* The event infrastructure notifies your extension when events of this type occur,
* passing the event object to the registered handler function.
*
* @param type The name of the event type of interest.
* @param listener The JavaScript handler function or method.
* @param obj Optional, the object containing the handler method, if any.
* Default is null.
*/
CSInterface.prototype.addEventListener = function(type, listener, obj)
{
window.__adobe_cep__.addEventListener(type, listener, obj);
};
/**
* Removes a registered event listener.
*
* @param type The name of the event type of interest.
* @param listener The JavaScript handler function or method that was registered.
* @param obj Optional, the object containing the handler method, if any.
* Default is null.
*/
CSInterface.prototype.removeEventListener = function(type, listener, obj)
{
window.__adobe_cep__.removeEventListener(type, listener, obj);
};
/**
* Loads and launches another extension, or activates the extension if it is already loaded.
*
* @param extensionId The extension's unique identifier.
* @param startupParams Not currently used, pass "".
*
* @example
* To launch the extension "help" with ID "HLP" from this extension, call:
* requestOpenExtension("HLP", "");
*
*/
CSInterface.prototype.requestOpenExtension = function(extensionId, params)
{
window.__adobe_cep__.requestOpenExtension(extensionId, params);
};
/**
* Retrieves the list of extensions currently loaded in the current host application.
* The extension list is initialized once, and remains the same during the lifetime
* of the CEP session.
*
* @param extensionIds Optional, an array of unique identifiers for extensions of interest.
* If omitted, retrieves data for all extensions.
*
* @return Zero or more \c #Extension objects.
*/
CSInterface.prototype.getExtensions = function(extensionIds)
{
var extensionIdsStr = JSON.stringify(extensionIds);
var extensionsStr = window.__adobe_cep__.getExtensions(extensionIdsStr);
var extensions = JSON.parse(extensionsStr);
return extensions;
};
/**
* Retrieves network-related preferences.
*
* @return A JavaScript object containing network preferences.
*/
CSInterface.prototype.getNetworkPreferences = function()
{
var result = window.__adobe_cep__.getNetworkPreferences();
var networkPre = JSON.parse(result);
return networkPre;
};
/**
* Initializes the resource bundle for this extension with property values
* for the current application and locale.
* To support multiple locales, you must define a property file for each locale,
* containing keyed display-string values for that locale.
* See localization documentation for Extension Builder and related products.
*
* Keys can be in the
* form key.value="localized string", for use in HTML text elements.
* For example, in this input element, the localized \c key.value string is displayed
* instead of the empty \c value string:
*
*
*
* @return An object containing the resource bundle information.
*/
CSInterface.prototype.initResourceBundle = function()
{
var resourceBundle = JSON.parse(window.__adobe_cep__.initResourceBundle());
var resElms = document.querySelectorAll('[data-locale]');
for (var n = 0; n < resElms.length; n++)
{
var resEl = resElms[n];
// Get the resource key from the element.
var resKey = resEl.getAttribute('data-locale');
if (resKey)
{
// Get all the resources that start with the key.
for (var key in resourceBundle)
{
if (key.indexOf(resKey) === 0)
{
var resValue = resourceBundle[key];
if (key.length == resKey.length)
{
resEl.innerHTML = resValue;
}
else if ('.' == key.charAt(resKey.length))
{
var attrKey = key.substring(resKey.length + 1);
resEl[attrKey] = resValue;
}
}
}
}
}
return resourceBundle;
};
/**
* Writes installation information to a file.
*
* @return The file path.
*/
CSInterface.prototype.dumpInstallationInfo = function()
{
return window.__adobe_cep__.dumpInstallationInfo();
};
/**
* Retrieves version information for the current Operating System,
* See http://www.useragentstring.com/pages/Chrome/ for Chrome \c navigator.userAgent values.
*
* @return A string containing the OS version, or "unknown Operation System".
* If user customizes the User Agent by setting CEF command parameter "--user-agent", only
* "Mac OS X" or "Windows" will be returned.
*/
CSInterface.prototype.getOSInformation = function()
{
var userAgent = navigator.userAgent;
if ((navigator.platform == "Win32") || (navigator.platform == "Windows"))
{
var winVersion = "Windows";
var winBit = "";
if (userAgent.indexOf("Windows") > -1)
{
if (userAgent.indexOf("Windows NT 5.0") > -1)
{
winVersion = "Windows 2000";
}
else if (userAgent.indexOf("Windows NT 5.1") > -1)
{
winVersion = "Windows XP";
}
else if (userAgent.indexOf("Windows NT 5.2") > -1)
{
winVersion = "Windows Server 2003";
}
else if (userAgent.indexOf("Windows NT 6.0") > -1)
{
winVersion = "Windows Vista";
}
else if (userAgent.indexOf("Windows NT 6.1") > -1)
{
winVersion = "Windows 7";
}
else if (userAgent.indexOf("Windows NT 6.2") > -1)
{
winVersion = "Windows 8";
}
else if (userAgent.indexOf("Windows NT 6.3") > -1)
{
winVersion = "Windows 8.1";
}
else if (userAgent.indexOf("Windows NT 10") > -1)
{
winVersion = "Windows 10";
}
if (userAgent.indexOf("WOW64") > -1 || userAgent.indexOf("Win64") > -1)
{
winBit = " 64-bit";
}
else
{
winBit = " 32-bit";
}
}
return winVersion + winBit;
}
else if ((navigator.platform == "MacIntel") || (navigator.platform == "Macintosh"))
{
var result = "Mac OS X";
if (userAgent.indexOf("Mac OS X") > -1)
{
result = userAgent.substring(userAgent.indexOf("Mac OS X"), userAgent.indexOf(")"));
result = result.replace(/_/g, ".");
}
return result;
}
return "Unknown Operation System";
};
/**
* Opens a page in the default system browser.
*
* Since 4.2.0
*
* @param url The URL of the page/file to open, or the email address.
* Must use HTTP/HTTPS/file/mailto protocol. For example:
* "http://www.adobe.com"
* "https://github.com"
* "file:///C:/log.txt"
* "mailto:test@adobe.com"
*
* @return One of these error codes:\n
* \n
*
\n
*/
CSInterface.prototype.openURLInDefaultBrowser = function(url)
{
return cep.util.openURLInDefaultBrowser(url);
};
/**
* Retrieves extension ID.
*
* Since 4.2.0
*
* @return extension ID.
*/
CSInterface.prototype.getExtensionID = function()
{
return window.__adobe_cep__.getExtensionId();
};
/**
* Retrieves the scale factor of screen.
* On Windows platform, the value of scale factor might be different from operating system's scale factor,
* since host application may use its self-defined scale factor.
*
* Since 4.2.0
*
* @return One of the following float number.
* \n
*
\n
*/
CSInterface.prototype.getScaleFactor = function()
{
return window.__adobe_cep__.getScaleFactor();
};
/**
* Set a handler to detect any changes of scale factor. This only works on Mac.
*
* Since 4.2.0
*
* @param handler The function to be called when scale factor is changed.
*
*/
CSInterface.prototype.setScaleFactorChangedHandler = function(handler)
{
window.__adobe_cep__.setScaleFactorChangedHandler(handler);
};
/**
* Retrieves current API version.
*
* Since 4.2.0
*
* @return ApiVersion object.
*
*/
CSInterface.prototype.getCurrentApiVersion = function()
{
var apiVersion = JSON.parse(window.__adobe_cep__.getCurrentApiVersion());
return apiVersion;
};
/**
* Set panel flyout menu by an XML.
*
* Since 5.2.0
*
* Register a callback function for "com.adobe.csxs.events.flyoutMenuClicked" to get notified when a
* menu item is clicked.
* The "data" attribute of event is an object which contains "menuId" and "menuName" attributes.
*
* Register callback functions for "com.adobe.csxs.events.flyoutMenuOpened" and "com.adobe.csxs.events.flyoutMenuClosed"
* respectively to get notified when flyout menu is opened or closed.
*
* @param menu A XML string which describes menu structure.
* An example menu XML:
*
*
*/
CSInterface.prototype.setPanelFlyoutMenu = function(menu)
{
if ("string" != typeof menu)
{
return;
}
window.__adobe_cep__.invokeSync("setPanelFlyoutMenu", menu);
};
/**
* Updates a menu item in the extension window's flyout menu, by setting the enabled
* and selection status.
*
* Since 5.2.0
*
* @param menuItemLabel The menu item label.
* @param enabled True to enable the item, false to disable it (gray it out).
* @param checked True to select the item, false to deselect it.
*
* @return false when the host application does not support this functionality (HostCapabilities.EXTENDED_PANEL_MENU is false).
* Fails silently if menu label is invalid.
*
* @see HostCapabilities.EXTENDED_PANEL_MENU
*/
CSInterface.prototype.updatePanelMenuItem = function(menuItemLabel, enabled, checked)
{
var ret = false;
if (this.getHostCapabilities().EXTENDED_PANEL_MENU)
{
var itemStatus = new MenuItemStatus(menuItemLabel, enabled, checked);
ret = window.__adobe_cep__.invokeSync("updatePanelMenuItem", JSON.stringify(itemStatus));
}
return ret;
};
/**
* Set context menu by XML string.
*
* Since 5.2.0
*
* There are a number of conventions used to communicate what type of menu item to create and how it should be handled.
* - an item without menu ID or menu name is disabled and is not shown.
* - if the item name is "---" (three hyphens) then it is treated as a separator. The menu ID in this case will always be NULL.
* - Checkable attribute takes precedence over Checked attribute.
* - a PNG icon. For optimal display results please supply a 16 x 16px icon as larger dimensions will increase the size of the menu item.
The Chrome extension contextMenus API was taken as a reference.
https://developer.chrome.com/extensions/contextMenus
* - the items with icons and checkable items cannot coexist on the same menu level. The former take precedences over the latter.
*
* @param menu A XML string which describes menu structure.
* @param callback The callback function which is called when a menu item is clicked. The only parameter is the returned ID of clicked menu item.
*
* @description An example menu XML:
*
*/
CSInterface.prototype.setContextMenu = function(menu, callback)
{
if ("string" != typeof menu)
{
return;
}
window.__adobe_cep__.invokeAsync("setContextMenu", menu, callback);
};
/**
* Set context menu by JSON string.
*
* Since 6.0.0
*
* There are a number of conventions used to communicate what type of menu item to create and how it should be handled.
* - an item without menu ID or menu name is disabled and is not shown.
* - if the item label is "---" (three hyphens) then it is treated as a separator. The menu ID in this case will always be NULL.
* - Checkable attribute takes precedence over Checked attribute.
* - a PNG icon. For optimal display results please supply a 16 x 16px icon as larger dimensions will increase the size of the menu item.
The Chrome extension contextMenus API was taken as a reference.
* - the items with icons and checkable items cannot coexist on the same menu level. The former take precedences over the latter.
https://developer.chrome.com/extensions/contextMenus
*
* @param menu A JSON string which describes menu structure.
* @param callback The callback function which is called when a menu item is clicked. The only parameter is the returned ID of clicked menu item.
*
* @description An example menu JSON:
*
* {
* "menu": [
* {
* "id": "menuItemId1",
* "label": "testExample1",
* "enabled": true,
* "checkable": true,
* "checked": false,
* "icon": "./image/small_16X16.png"
* },
* {
* "id": "menuItemId2",
* "label": "testExample2",
* "menu": [
* {
* "id": "menuItemId2-1",
* "label": "testExample2-1",
* "menu": [
* {
* "id": "menuItemId2-1-1",
* "label": "testExample2-1-1",
* "enabled": false,
* "checkable": true,
* "checked": true
* }
* ]
* },
* {
* "id": "menuItemId2-2",
* "label": "testExample2-2",
* "enabled": true,
* "checkable": true,
* "checked": true
* }
* ]
* },
* {
* "label": "---"
* },
* {
* "id": "menuItemId3",
* "label": "testExample3",
* "enabled": false,
* "checkable": true,
* "checked": false
* }
* ]
* }
*
*/
CSInterface.prototype.setContextMenuByJSON = function(menu, callback)
{
if ("string" != typeof menu)
{
return;
}
window.__adobe_cep__.invokeAsync("setContextMenuByJSON", menu, callback);
};
/**
* Updates a context menu item by setting the enabled and selection status.
*
* Since 5.2.0
*
* @param menuItemID The menu item ID.
* @param enabled True to enable the item, false to disable it (gray it out).
* @param checked True to select the item, false to deselect it.
*/
CSInterface.prototype.updateContextMenuItem = function(menuItemID, enabled, checked)
{
var itemStatus = new ContextMenuItemStatus(menuItemID, enabled, checked);
ret = window.__adobe_cep__.invokeSync("updateContextMenuItem", JSON.stringify(itemStatus));
};
/**
* Get the visibility status of an extension window.
*
* Since 6.0.0
*
* @return true if the extension window is visible; false if the extension window is hidden.
*/
CSInterface.prototype.isWindowVisible = function()
{
return window.__adobe_cep__.invokeSync("isWindowVisible", "");
};
/**
* Resize extension's content to the specified dimensions.
* 1. Works with modal and modeless extensions in all Adobe products.
* 2. Extension's manifest min/max size constraints apply and take precedence.
* 3. For panel extensions
* 3.1 This works in all Adobe products except:
* * Premiere Pro
* * Prelude
* * After Effects
* 3.2 When the panel is in certain states (especially when being docked),
* it will not change to the desired dimensions even when the
* specified size satisfies min/max constraints.
*
* Since 6.0.0
*
* @param width The new width
* @param height The new height
*/
CSInterface.prototype.resizeContent = function(width, height)
{
window.__adobe_cep__.resizeContent(width, height);
};
/**
* Register the invalid certificate callback for an extension.
* This callback will be triggered when the extension tries to access the web site that contains the invalid certificate on the main frame.
* But if the extension does not call this function and tries to access the web site containing the invalid certificate, a default error page will be shown.
*
* Since 6.1.0
*
* @param callback the callback function
*/
CSInterface.prototype.registerInvalidCertificateCallback = function(callback)
{
return window.__adobe_cep__.registerInvalidCertificateCallback(callback);
};
/**
* Register an interest in some key events to prevent them from being sent to the host application.
*
* This function works with modeless extensions and panel extensions.
* Generally all the key events will be sent to the host application for these two extensions if the current focused element
* is not text input or dropdown,
* If you want to intercept some key events and want them to be handled in the extension, please call this function
* in advance to prevent them being sent to the host application.
*
* Since 6.1.0
*
* @param keyEventsInterest A JSON string describing those key events you are interested in. A null object or
an empty string will lead to removing the interest
*
* This JSON string should be an array, each object has following keys:
*
* keyCode: [Required] represents an OS system dependent virtual key code identifying
* the unmodified value of the pressed key.
* ctrlKey: [optional] a Boolean that indicates if the control key was pressed (true) or not (false) when the event occurred.
* altKey: [optional] a Boolean that indicates if the alt key was pressed (true) or not (false) when the event occurred.
* shiftKey: [optional] a Boolean that indicates if the shift key was pressed (true) or not (false) when the event occurred.
* metaKey: [optional] (Mac Only) a Boolean that indicates if the Meta key was pressed (true) or not (false) when the event occurred.
* On Macintosh keyboards, this is the command key. To detect Windows key on Windows, please use keyCode instead.
* An example JSON string:
*
* [
* {
* "keyCode": 48
* },
* {
* "keyCode": 123,
* "ctrlKey": true
* },
* {
* "keyCode": 123,
* "ctrlKey": true,
* "metaKey": true
* }
* ]
*
*/
CSInterface.prototype.registerKeyEventsInterest = function(keyEventsInterest)
{
return window.__adobe_cep__.registerKeyEventsInterest(keyEventsInterest);
};
/**
* Set the title of the extension window.
* This function works with modal and modeless extensions in all Adobe products, and panel extensions in Photoshop, InDesign, InCopy, Illustrator, Flash Pro and Dreamweaver.
*
* Since 6.1.0
*
* @param title The window title.
*/
CSInterface.prototype.setWindowTitle = function(title)
{
window.__adobe_cep__.invokeSync("setWindowTitle", title);
};
/**
* Get the title of the extension window.
* This function works with modal and modeless extensions in all Adobe products, and panel extensions in Photoshop, InDesign, InCopy, Illustrator, Flash Pro and Dreamweaver.
*
* Since 6.1.0
*
* @return The window title.
*/
CSInterface.prototype.getWindowTitle = function()
{
return window.__adobe_cep__.invokeSync("getWindowTitle", "");
};
================================================
FILE: openpype/hosts/aftereffects/api/extension/js/libs/json.js
================================================
// json2.js
// 2017-06-12
// Public Domain.
// NO WARRANTY EXPRESSED OR IMPLIED. USE AT YOUR OWN RISK.
// USE YOUR OWN COPY. IT IS EXTREMELY UNWISE TO LOAD CODE FROM SERVERS YOU DO
// NOT CONTROL.
// This file creates a global JSON object containing two methods: stringify
// and parse. This file provides the ES5 JSON capability to ES3 systems.
// If a project might run on IE8 or earlier, then this file should be included.
// This file does nothing on ES5 systems.
// JSON.stringify(value, replacer, space)
// value any JavaScript value, usually an object or array.
// replacer an optional parameter that determines how object
// values are stringified for objects. It can be a
// function or an array of strings.
// space an optional parameter that specifies the indentation
// of nested structures. If it is omitted, the text will
// be packed without extra whitespace. If it is a number,
// it will specify the number of spaces to indent at each
// level. If it is a string (such as "\t" or " "),
// it contains the characters used to indent at each level.
// This method produces a JSON text from a JavaScript value.
// When an object value is found, if the object contains a toJSON
// method, its toJSON method will be called and the result will be
// stringified. A toJSON method does not serialize: it returns the
// value represented by the name/value pair that should be serialized,
// or undefined if nothing should be serialized. The toJSON method
// will be passed the key associated with the value, and this will be
// bound to the value.
// For example, this would serialize Dates as ISO strings.
// Date.prototype.toJSON = function (key) {
// function f(n) {
// // Format integers to have at least two digits.
// return (n < 10)
// ? "0" + n
// : n;
// }
// return this.getUTCFullYear() + "-" +
// f(this.getUTCMonth() + 1) + "-" +
// f(this.getUTCDate()) + "T" +
// f(this.getUTCHours()) + ":" +
// f(this.getUTCMinutes()) + ":" +
// f(this.getUTCSeconds()) + "Z";
// };
// You can provide an optional replacer method. It will be passed the
// key and value of each member, with this bound to the containing
// object. The value that is returned from your method will be
// serialized. If your method returns undefined, then the member will
// be excluded from the serialization.
// If the replacer parameter is an array of strings, then it will be
// used to select the members to be serialized. It filters the results
// such that only members with keys listed in the replacer array are
// stringified.
// Values that do not have JSON representations, such as undefined or
// functions, will not be serialized. Such values in objects will be
// dropped; in arrays they will be replaced with null. You can use
// a replacer function to replace those with JSON values.
// JSON.stringify(undefined) returns undefined.
// The optional space parameter produces a stringification of the
// value that is filled with line breaks and indentation to make it
// easier to read.
// If the space parameter is a non-empty string, then that string will
// be used for indentation. If the space parameter is a number, then
// the indentation will be that many spaces.
// Example:
// text = JSON.stringify(["e", {pluribus: "unum"}]);
// // text is '["e",{"pluribus":"unum"}]'
// text = JSON.stringify(["e", {pluribus: "unum"}], null, "\t");
// // text is '[\n\t"e",\n\t{\n\t\t"pluribus": "unum"\n\t}\n]'
// text = JSON.stringify([new Date()], function (key, value) {
// return this[key] instanceof Date
// ? "Date(" + this[key] + ")"
// : value;
// });
// // text is '["Date(---current time---)"]'
// JSON.parse(text, reviver)
// This method parses a JSON text to produce an object or array.
// It can throw a SyntaxError exception.
// The optional reviver parameter is a function that can filter and
// transform the results. It receives each of the keys and values,
// and its return value is used instead of the original value.
// If it returns what it received, then the structure is not modified.
// If it returns undefined then the member is deleted.
// Example:
// // Parse the text. Values that look like ISO date strings will
// // be converted to Date objects.
// myData = JSON.parse(text, function (key, value) {
// var a;
// if (typeof value === "string") {
// a =
// /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2}(?:\.\d*)?)Z$/.exec(value);
// if (a) {
// return new Date(Date.UTC(
// +a[1], +a[2] - 1, +a[3], +a[4], +a[5], +a[6]
// ));
// }
// return value;
// }
// });
// myData = JSON.parse(
// "[\"Date(09/09/2001)\"]",
// function (key, value) {
// var d;
// if (
// typeof value === "string"
// && value.slice(0, 5) === "Date("
// && value.slice(-1) === ")"
// ) {
// d = new Date(value.slice(5, -1));
// if (d) {
// return d;
// }
// }
// return value;
// }
// );
// This is a reference implementation. You are free to copy, modify, or
// redistribute.
/*jslint
eval, for, this
*/
/*property
JSON, apply, call, charCodeAt, getUTCDate, getUTCFullYear, getUTCHours,
getUTCMinutes, getUTCMonth, getUTCSeconds, hasOwnProperty, join,
lastIndex, length, parse, prototype, push, replace, slice, stringify,
test, toJSON, toString, valueOf
*/
// Create a JSON object only if one does not already exist. We create the
// methods in a closure to avoid creating global variables.
if (typeof JSON !== "object") {
JSON = {};
}
(function () {
"use strict";
var rx_one = /^[\],:{}\s]*$/;
var rx_two = /\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g;
var rx_three = /"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g;
var rx_four = /(?:^|:|,)(?:\s*\[)+/g;
var rx_escapable = /[\\"\u0000-\u001f\u007f-\u009f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g;
var rx_dangerous = /[\u0000\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g;
function f(n) {
// Format integers to have at least two digits.
return (n < 10)
? "0" + n
: n;
}
function this_value() {
return this.valueOf();
}
if (typeof Date.prototype.toJSON !== "function") {
Date.prototype.toJSON = function () {
return isFinite(this.valueOf())
? (
this.getUTCFullYear()
+ "-"
+ f(this.getUTCMonth() + 1)
+ "-"
+ f(this.getUTCDate())
+ "T"
+ f(this.getUTCHours())
+ ":"
+ f(this.getUTCMinutes())
+ ":"
+ f(this.getUTCSeconds())
+ "Z"
)
: null;
};
Boolean.prototype.toJSON = this_value;
Number.prototype.toJSON = this_value;
String.prototype.toJSON = this_value;
}
var gap;
var indent;
var meta;
var rep;
function quote(string) {
// If the string contains no control characters, no quote characters, and no
// backslash characters, then we can safely slap some quotes around it.
// Otherwise we must also replace the offending characters with safe escape
// sequences.
rx_escapable.lastIndex = 0;
return rx_escapable.test(string)
? "\"" + string.replace(rx_escapable, function (a) {
var c = meta[a];
return typeof c === "string"
? c
: "\\u" + ("0000" + a.charCodeAt(0).toString(16)).slice(-4);
}) + "\""
: "\"" + string + "\"";
}
function str(key, holder) {
// Produce a string from holder[key].
var i; // The loop counter.
var k; // The member key.
var v; // The member value.
var length;
var mind = gap;
var partial;
var value = holder[key];
// If the value has a toJSON method, call it to obtain a replacement value.
if (
value
&& typeof value === "object"
&& typeof value.toJSON === "function"
) {
value = value.toJSON(key);
}
// If we were called with a replacer function, then call the replacer to
// obtain a replacement value.
if (typeof rep === "function") {
value = rep.call(holder, key, value);
}
// What happens next depends on the value's type.
switch (typeof value) {
case "string":
return quote(value);
case "number":
// JSON numbers must be finite. Encode non-finite numbers as null.
return (isFinite(value))
? String(value)
: "null";
case "boolean":
case "null":
// If the value is a boolean or null, convert it to a string. Note:
// typeof null does not produce "null". The case is included here in
// the remote chance that this gets fixed someday.
return String(value);
// If the type is "object", we might be dealing with an object or an array or
// null.
case "object":
// Due to a specification blunder in ECMAScript, typeof null is "object",
// so watch out for that case.
if (!value) {
return "null";
}
// Make an array to hold the partial results of stringifying this object value.
gap += indent;
partial = [];
// Is the value an array?
if (Object.prototype.toString.apply(value) === "[object Array]") {
// The value is an array. Stringify every element. Use null as a placeholder
// for non-JSON values.
length = value.length;
for (i = 0; i < length; i += 1) {
partial[i] = str(i, value) || "null";
}
// Join all of the elements together, separated with commas, and wrap them in
// brackets.
v = partial.length === 0
? "[]"
: gap
? (
"[\n"
+ gap
+ partial.join(",\n" + gap)
+ "\n"
+ mind
+ "]"
)
: "[" + partial.join(",") + "]";
gap = mind;
return v;
}
// If the replacer is an array, use it to select the members to be stringified.
if (rep && typeof rep === "object") {
length = rep.length;
for (i = 0; i < length; i += 1) {
if (typeof rep[i] === "string") {
k = rep[i];
v = str(k, value);
if (v) {
partial.push(quote(k) + (
(gap)
? ": "
: ":"
) + v);
}
}
}
} else {
// Otherwise, iterate through all of the keys in the object.
for (k in value) {
if (Object.prototype.hasOwnProperty.call(value, k)) {
v = str(k, value);
if (v) {
partial.push(quote(k) + (
(gap)
? ": "
: ":"
) + v);
}
}
}
}
// Join all of the member texts together, separated with commas,
// and wrap them in braces.
v = partial.length === 0
? "{}"
: gap
? "{\n" + gap + partial.join(",\n" + gap) + "\n" + mind + "}"
: "{" + partial.join(",") + "}";
gap = mind;
return v;
}
}
// If the JSON object does not yet have a stringify method, give it one.
if (typeof JSON.stringify !== "function") {
meta = { // table of character substitutions
"\b": "\\b",
"\t": "\\t",
"\n": "\\n",
"\f": "\\f",
"\r": "\\r",
"\"": "\\\"",
"\\": "\\\\"
};
JSON.stringify = function (value, replacer, space) {
// The stringify method takes a value and an optional replacer, and an optional
// space parameter, and returns a JSON text. The replacer can be a function
// that can replace values, or an array of strings that will select the keys.
// A default replacer method can be provided. Use of the space parameter can
// produce text that is more easily readable.
var i;
gap = "";
indent = "";
// If the space parameter is a number, make an indent string containing that
// many spaces.
if (typeof space === "number") {
for (i = 0; i < space; i += 1) {
indent += " ";
}
// If the space parameter is a string, it will be used as the indent string.
} else if (typeof space === "string") {
indent = space;
}
// If there is a replacer, it must be a function or an array.
// Otherwise, throw an error.
rep = replacer;
if (replacer && typeof replacer !== "function" && (
typeof replacer !== "object"
|| typeof replacer.length !== "number"
)) {
throw new Error("JSON.stringify");
}
// Make a fake root object containing our value under the key of "".
// Return the result of stringifying the value.
return str("", {"": value});
};
}
// If the JSON object does not yet have a parse method, give it one.
if (typeof JSON.parse !== "function") {
JSON.parse = function (text, reviver) {
// The parse method takes a text and an optional reviver function, and returns
// a JavaScript value if the text is a valid JSON text.
var j;
function walk(holder, key) {
// The walk method is used to recursively walk the resulting structure so
// that modifications can be made.
var k;
var v;
var value = holder[key];
if (value && typeof value === "object") {
for (k in value) {
if (Object.prototype.hasOwnProperty.call(value, k)) {
v = walk(value, k);
if (v !== undefined) {
value[k] = v;
} else {
delete value[k];
}
}
}
}
return reviver.call(holder, key, value);
}
// Parsing happens in four stages. In the first stage, we replace certain
// Unicode characters with escape sequences. JavaScript handles many characters
// incorrectly, either silently deleting them, or treating them as line endings.
text = String(text);
rx_dangerous.lastIndex = 0;
if (rx_dangerous.test(text)) {
text = text.replace(rx_dangerous, function (a) {
return (
"\\u"
+ ("0000" + a.charCodeAt(0).toString(16)).slice(-4)
);
});
}
// In the second stage, we run the text against regular expressions that look
// for non-JSON patterns. We are especially concerned with "()" and "new"
// because they can cause invocation, and "=" because it can cause mutation.
// But just to be safe, we want to reject all unexpected forms.
// We split the second stage into 4 regexp operations in order to work around
// crippling inefficiencies in IE's and Safari's regexp engines. First we
// replace the JSON backslash pairs with "@" (a non-JSON character). Second, we
// replace all simple value tokens with "]" characters. Third, we delete all
// open brackets that follow a colon or comma or that begin the text. Finally,
// we look to see that the remaining characters are only whitespace or "]" or
// "," or ":" or "{" or "}". If that is so, then the text is safe for eval.
if (
rx_one.test(
text
.replace(rx_two, "@")
.replace(rx_three, "]")
.replace(rx_four, "")
)
) {
// In the third stage we use the eval function to compile the text into a
// JavaScript structure. The "{" operator is subject to a syntactic ambiguity
// in JavaScript: it can begin a block or an object literal. We wrap the text
// in parens to eliminate the ambiguity.
j = eval("(" + text + ")");
// In the optional fourth stage, we recursively walk the new structure, passing
// each name/value pair to a reviver function for possible transformation.
return (typeof reviver === "function")
? walk({"": j}, "")
: j;
}
// If the text is not JSON parseable, then a SyntaxError is thrown.
throw new SyntaxError("JSON.parse");
};
}
}());
================================================
FILE: openpype/hosts/aftereffects/api/extension/js/libs/wsrpc.js
================================================
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
typeof define === 'function' && define.amd ? define(factory) :
(global = global || self, global.WSRPC = factory());
}(this, function () { 'use strict';
function _classCallCheck(instance, Constructor) {
if (!(instance instanceof Constructor)) {
throw new TypeError("Cannot call a class as a function");
}
}
var Deferred = function Deferred() {
_classCallCheck(this, Deferred);
var self = this;
self.resolve = null;
self.reject = null;
self.done = false;
function wrapper(func) {
return function () {
if (self.done) throw new Error('Promise already done');
self.done = true;
return func.apply(this, arguments);
};
}
self.promise = new Promise(function (resolve, reject) {
self.resolve = wrapper(resolve);
self.reject = wrapper(reject);
});
self.promise.isPending = function () {
return !self.done;
};
return self;
};
function logGroup(group, level, args) {
console.group(group);
console[level].apply(this, args);
console.groupEnd();
}
function log() {
if (!WSRPC.DEBUG) return;
logGroup('WSRPC.DEBUG', 'trace', arguments);
}
function trace(msg) {
if (!WSRPC.TRACE) return;
var payload = msg;
if ('data' in msg) payload = JSON.parse(msg.data);
logGroup("WSRPC.TRACE", 'trace', [payload]);
}
function getAbsoluteWsUrl(url) {
if (/^\w+:\/\//.test(url)) return url;
if (typeof window == 'undefined' && window.location.host.length < 1) throw new Error("Can not construct absolute URL from ".concat(window.location));
var scheme = window.location.protocol === "https:" ? "wss:" : "ws:";
var port = window.location.port === '' ? ":".concat(window.location.port) : '';
var host = window.location.host;
var path = url.replace(/^\/+/gm, '');
return "".concat(scheme, "//").concat(host).concat(port, "/").concat(path);
}
var readyState = Object.freeze({
0: 'CONNECTING',
1: 'OPEN',
2: 'CLOSING',
3: 'CLOSED'
});
var WSRPC = function WSRPC(URL) {
var reconnectTimeout = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 1000;
_classCallCheck(this, WSRPC);
var self = this;
URL = getAbsoluteWsUrl(URL);
self.id = 1;
self.eventId = 0;
self.socketStarted = false;
self.eventStore = {
onconnect: {},
onerror: {},
onclose: {},
onchange: {}
};
self.connectionNumber = 0;
self.oneTimeEventStore = {
onconnect: [],
onerror: [],
onclose: [],
onchange: []
};
self.callQueue = [];
function createSocket() {
var ws = new WebSocket(URL);
var rejectQueue = function rejectQueue() {
self.connectionNumber++; // rejects incoming calls
var deferred; //reject all pending calls
while (0 < self.callQueue.length) {
var callObj = self.callQueue.shift();
deferred = self.store[callObj.id];
delete self.store[callObj.id];
if (deferred && deferred.promise.isPending()) {
deferred.reject('WebSocket error occurred');
}
} // reject all from the store
for (var key in self.store) {
if (!self.store.hasOwnProperty(key)) continue;
deferred = self.store[key];
if (deferred && deferred.promise.isPending()) {
deferred.reject('WebSocket error occurred');
}
}
};
function reconnect(callEvents) {
setTimeout(function () {
try {
self.socket = createSocket();
self.id = 1;
} catch (exc) {
callEvents('onerror', exc);
delete self.socket;
console.error(exc);
}
}, reconnectTimeout);
}
ws.onclose = function (err) {
log('ONCLOSE CALLED', 'STATE', self.public.state());
trace(err);
for (var serial in self.store) {
if (!self.store.hasOwnProperty(serial)) continue;
if (self.store[serial].hasOwnProperty('reject')) {
self.store[serial].reject('Connection closed');
}
}
rejectQueue();
callEvents('onclose', err);
callEvents('onchange', err);
reconnect(callEvents);
};
ws.onerror = function (err) {
log('ONERROR CALLED', 'STATE', self.public.state());
trace(err);
rejectQueue();
callEvents('onerror', err);
callEvents('onchange', err);
log('WebSocket has been closed by error: ', err);
};
function tryCallEvent(func, event) {
try {
return func(event);
} catch (e) {
if (e.hasOwnProperty('stack')) {
log(e.stack);
} else {
log('Event function', func, 'raised unknown error:', e);
}
console.error(e);
}
}
function callEvents(evName, event) {
while (0 < self.oneTimeEventStore[evName].length) {
var deferred = self.oneTimeEventStore[evName].shift();
if (deferred.hasOwnProperty('resolve') && deferred.promise.isPending()) deferred.resolve();
}
for (var i in self.eventStore[evName]) {
if (!self.eventStore[evName].hasOwnProperty(i)) continue;
var cur = self.eventStore[evName][i];
tryCallEvent(cur, event);
}
}
ws.onopen = function (ev) {
log('ONOPEN CALLED', 'STATE', self.public.state());
trace(ev);
while (0 < self.callQueue.length) {
// noinspection JSUnresolvedFunction
self.socket.send(JSON.stringify(self.callQueue.shift(), 0, 1));
}
callEvents('onconnect', ev);
callEvents('onchange', ev);
};
function handleCall(self, data) {
if (!self.routes.hasOwnProperty(data.method)) throw new Error('Route not found');
var connectionNumber = self.connectionNumber;
var deferred = new Deferred();
deferred.promise.then(function (result) {
if (connectionNumber !== self.connectionNumber) return;
self.socket.send(JSON.stringify({
id: data.id,
result: result
}));
}, function (error) {
if (connectionNumber !== self.connectionNumber) return;
self.socket.send(JSON.stringify({
id: data.id,
error: error
}));
});
var func = self.routes[data.method];
if (self.asyncRoutes[data.method]) return func.apply(deferred, [data.params]);
function badPromise() {
throw new Error("You should register route with async flag.");
}
var promiseMock = {
resolve: badPromise,
reject: badPromise
};
try {
deferred.resolve(func.apply(promiseMock, [data.params]));
} catch (e) {
deferred.reject(e);
console.error(e);
}
}
function handleError(self, data) {
if (!self.store.hasOwnProperty(data.id)) return log('Unknown callback');
var deferred = self.store[data.id];
if (typeof deferred === 'undefined') return log('Confirmation without handler');
delete self.store[data.id];
log('REJECTING', data.error);
deferred.reject(data.error);
}
function handleResult(self, data) {
var deferred = self.store[data.id];
if (typeof deferred === 'undefined') return log('Confirmation without handler');
delete self.store[data.id];
if (data.hasOwnProperty('result')) {
return deferred.resolve(data.result);
}
return deferred.reject(data.error);
}
ws.onmessage = function (message) {
log('ONMESSAGE CALLED', 'STATE', self.public.state());
trace(message);
if (message.type !== 'message') return;
var data;
try {
data = JSON.parse(message.data);
log(data);
if (data.hasOwnProperty('method')) {
return handleCall(self, data);
} else if (data.hasOwnProperty('error') && data.error === null) {
return handleError(self, data);
} else {
return handleResult(self, data);
}
} catch (exception) {
var err = {
error: exception.message,
result: null,
id: data ? data.id : null
};
self.socket.send(JSON.stringify(err));
console.error(exception);
}
};
return ws;
}
function makeCall(func, args, params) {
self.id += 2;
var deferred = new Deferred();
var callObj = Object.freeze({
id: self.id,
method: func,
params: args
});
var state = self.public.state();
if (state === 'OPEN') {
self.store[self.id] = deferred;
self.socket.send(JSON.stringify(callObj));
} else if (state === 'CONNECTING') {
log('SOCKET IS', state);
self.store[self.id] = deferred;
self.callQueue.push(callObj);
} else {
log('SOCKET IS', state);
if (params && params['noWait']) {
deferred.reject("Socket is: ".concat(state));
} else {
self.store[self.id] = deferred;
self.callQueue.push(callObj);
}
}
return deferred.promise;
}
self.asyncRoutes = {};
self.routes = {};
self.store = {};
self.public = Object.freeze({
call: function call(func, args, params) {
return makeCall(func, args, params);
},
addRoute: function addRoute(route, callback, isAsync) {
self.asyncRoutes[route] = isAsync || false;
self.routes[route] = callback;
},
deleteRoute: function deleteRoute(route) {
delete self.asyncRoutes[route];
return delete self.routes[route];
},
addEventListener: function addEventListener(event, func) {
var eventId = self.eventId++;
self.eventStore[event][eventId] = func;
return eventId;
},
removeEventListener: function removeEventListener(event, index) {
if (self.eventStore[event].hasOwnProperty(index)) {
delete self.eventStore[event][index];
return true;
} else {
return false;
}
},
onEvent: function onEvent(event) {
var deferred = new Deferred();
self.oneTimeEventStore[event].push(deferred);
return deferred.promise;
},
destroy: function destroy() {
return self.socket.close();
},
state: function state() {
return readyState[this.stateCode()];
},
stateCode: function stateCode() {
if (self.socketStarted && self.socket) return self.socket.readyState;
return 3;
},
connect: function connect() {
self.socketStarted = true;
self.socket = createSocket();
}
});
self.public.addRoute('log', function (argsObj) {
//console.info("Websocket sent: ".concat(argsObj));
});
self.public.addRoute('ping', function (data) {
return data;
});
return self.public;
};
WSRPC.DEBUG = false;
WSRPC.TRACE = false;
return WSRPC;
}));
//# sourceMappingURL=wsrpc.js.map
================================================
FILE: openpype/hosts/aftereffects/api/extension/js/main.js
================================================
/*jslint vars: true, plusplus: true, devel: true, nomen: true, regexp: true,
indent: 4, maxerr: 50 */
/*global $, window, location, CSInterface, SystemPath, themeManager*/
var csInterface = new CSInterface();
log.warn("script start");
WSRPC.DEBUG = false;
WSRPC.TRACE = false;
// get websocket server url from environment value
async function startUp(url){
promis = runEvalScript("getEnv('" + url + "')");
var res = await promis;
log.warn("res: " + res);
promis = runEvalScript("getEnv('OPENPYPE_DEBUG')");
var debug = await promis;
log.warn("debug: " + debug);
if (debug && debug.toString() == '3'){
WSRPC.DEBUG = true;
WSRPC.TRACE = true;
}
// run rest only after resolved promise
main(res);
}
function get_extension_version(){
/** Returns version number from extension manifest.xml **/
log.debug("get_extension_version")
var path = csInterface.getSystemPath(SystemPath.EXTENSION);
log.debug("extension path " + path);
var result = window.cep.fs.readFile(path + "/CSXS/manifest.xml");
var version = undefined;
if(result.err === 0){
if (window.DOMParser) {
const parser = new DOMParser();
const xmlDoc = parser.parseFromString(result.data.toString(),
'text/xml');
const children = xmlDoc.children;
for (let i = 0; i <= children.length; i++) {
if (children[i] &&
children[i].getAttribute('ExtensionBundleVersion')) {
version =
children[i].getAttribute('ExtensionBundleVersion');
}
}
}
}
return '{"result":"' + version + '"}'
}
function main(websocket_url){
// creates connection to 'websocket_url', registers routes
var default_url = 'ws://localhost:8099/ws/';
if (websocket_url == ''){
websocket_url = default_url;
}
RPC = new WSRPC(websocket_url, 5000); // spin connection
RPC.connect();
log.warn("connected");
RPC.addRoute('AfterEffects.open', function (data) {
log.warn('Server called client route "open":', data);
var escapedPath = EscapeStringForJSX(data.path);
return runEvalScript("fileOpen('" + escapedPath +"')")
.then(function(result){
log.warn("open: " + result);
return result;
});
});
RPC.addRoute('AfterEffects.get_metadata', function (data) {
log.warn('Server called client route "get_metadata":', data);
return runEvalScript("getMetadata()")
.then(function(result){
log.warn("getMetadata: " + result);
return result;
});
});
RPC.addRoute('AfterEffects.get_active_document_name', function (data) {
log.warn('Server called client route ' +
'"get_active_document_name":', data);
return runEvalScript("getActiveDocumentName()")
.then(function(result){
log.warn("get_active_document_name: " + result);
return result;
});
});
RPC.addRoute('AfterEffects.get_active_document_full_name', function (data){
log.warn('Server called client route ' +
'"get_active_document_full_name":', data);
return runEvalScript("getActiveDocumentFullName()")
.then(function(result){
log.warn("get_active_document_full_name: " + result);
return result;
});
});
RPC.addRoute('AfterEffects.add_item', function (data) {
log.warn('Server called client route "add_item":', data);
var escapedName = EscapeStringForJSX(data.name);
return runEvalScript("addItem('" + escapedName +"', " +
"'" + data.item_type + "')")
.then(function(result){
log.warn("get_items: " + result);
return result;
});
});
RPC.addRoute('AfterEffects.get_items', function (data) {
log.warn('Server called client route "get_items":', data);
return runEvalScript("getItems(" + data.comps + "," +
data.folders + "," +
data.footages + ")")
.then(function(result){
log.warn("get_items: " + result);
return result;
});
});
RPC.addRoute('AfterEffects.select_items', function (data) {
log.warn('Server called client route "select_items":', data);
return runEvalScript("selectItems(" + JSON.stringify(data.items) + ")")
.then(function(result){
log.warn("select_items: " + result);
return result;
});
});
RPC.addRoute('AfterEffects.get_selected_items', function (data) {
log.warn('Server called client route "get_selected_items":', data);
return runEvalScript("getSelectedItems(" + data.comps + "," +
data.folders + "," +
data.footages + ")")
.then(function(result){
log.warn("get_items: " + result);
return result;
});
});
RPC.addRoute('AfterEffects.import_file', function (data) {
log.warn('Server called client route "import_file":', data);
var escapedPath = EscapeStringForJSX(data.path);
return runEvalScript("importFile('" + escapedPath +"', " +
"'" + data.item_name + "'," +
"'" + JSON.stringify(
data.import_options) + "')")
.then(function(result){
log.warn("importFile: " + result);
return result;
});
});
RPC.addRoute('AfterEffects.replace_item', function (data) {
log.warn('Server called client route "replace_item":', data);
var escapedPath = EscapeStringForJSX(data.path);
return runEvalScript("replaceItem(" + data.item_id + ", " +
"'" + escapedPath + "', " +
"'" + data.item_name + "')")
.then(function(result){
log.warn("replaceItem: " + result);
return result;
});
});
RPC.addRoute('AfterEffects.rename_item', function (data) {
log.warn('Server called client route "rename_item":', data);
return runEvalScript("renameItem(" + data.item_id + ", " +
"'" + data.item_name + "')")
.then(function(result){
log.warn("renameItem: " + result);
return result;
});
});
RPC.addRoute('AfterEffects.delete_item', function (data) {
log.warn('Server called client route "delete_item":', data);
return runEvalScript("deleteItem(" + data.item_id + ")")
.then(function(result){
log.warn("deleteItem: " + result);
return result;
});
});
RPC.addRoute('AfterEffects.imprint', function (data) {
log.warn('Server called client route "imprint":', data);
var escaped = data.payload.replace(/\n/g, "\\n");
return runEvalScript("imprint('" + escaped +"')")
.then(function(result){
log.warn("imprint: " + result);
return result;
});
});
RPC.addRoute('AfterEffects.set_label_color', function (data) {
log.warn('Server called client route "set_label_color":', data);
return runEvalScript("setLabelColor(" + data.item_id + "," +
data.color_idx + ")")
.then(function(result){
log.warn("imprint: " + result);
return result;
});
});
RPC.addRoute('AfterEffects.get_comp_properties', function (data) {
log.warn('Server called client route "get_comp_properties":', data);
return runEvalScript("getCompProperties(" + data.item_id + ")")
.then(function(result){
log.warn("get_comp_properties: " + result);
return result;
});
});
RPC.addRoute('AfterEffects.set_comp_properties', function (data) {
log.warn('Server called client route "set_work_area":', data);
return runEvalScript("setCompProperties(" + data.item_id + ',' +
data.start + ',' +
data.duration + ',' +
data.frame_rate + ',' +
data.width + ',' +
data.height + ")")
.then(function(result){
log.warn("set_comp_properties: " + result);
return result;
});
});
RPC.addRoute('AfterEffects.saveAs', function (data) {
log.warn('Server called client route "saveAs":', data);
var escapedPath = EscapeStringForJSX(data.image_path);
return runEvalScript("saveAs('" + escapedPath + "', " +
data.as_copy + ")")
.then(function(result){
log.warn("saveAs: " + result);
return result;
});
});
RPC.addRoute('AfterEffects.save', function (data) {
log.warn('Server called client route "save":', data);
return runEvalScript("save()")
.then(function(result){
log.warn("save: " + result);
return result;
});
});
RPC.addRoute('AfterEffects.get_render_info', function (data) {
log.warn('Server called client route "get_render_info":', data);
return runEvalScript("getRenderInfo(" + data.comp_id +")")
.then(function(result){
log.warn("get_render_info: " + result);
return result;
});
});
RPC.addRoute('AfterEffects.get_audio_url', function (data) {
log.warn('Server called client route "get_audio_url":', data);
return runEvalScript("getAudioUrlForComp(" + data.item_id + ")")
.then(function(result){
log.warn("getAudioUrlForComp: " + result);
return result;
});
});
RPC.addRoute('AfterEffects.import_background', function (data) {
log.warn('Server called client route "import_background":', data);
return runEvalScript("importBackground(" + data.comp_id + ", " +
"'" + data.comp_name + "', " +
JSON.stringify(data.files) + ")")
.then(function(result){
log.warn("importBackground: " + result);
return result;
});
});
RPC.addRoute('AfterEffects.reload_background', function (data) {
log.warn('Server called client route "reload_background":', data);
return runEvalScript("reloadBackground(" + data.comp_id + ", " +
"'" + data.comp_name + "', " +
JSON.stringify(data.files) + ")")
.then(function(result){
log.warn("reloadBackground: " + result);
return result;
});
});
RPC.addRoute('AfterEffects.add_item_as_layer', function (data) {
log.warn('Server called client route "add_item_as_layer":', data);
return runEvalScript("addItemAsLayerToComp(" + data.comp_id + ", " +
data.item_id + "," +
" null )")
.then(function(result){
log.warn("addItemAsLayerToComp: " + result);
return result;
});
});
RPC.addRoute('AfterEffects.add_item_instead_placeholder', function (data) {
log.warn('Server called client route "add_item_instead_placeholder":', data);
return runEvalScript("addItemInstead(" + data.placeholder_item_id + ", " +
data.item_id + ")")
.then(function(result){
log.warn("add_item_instead_placeholder: " + result);
return result;
});
});
RPC.addRoute('AfterEffects.render', function (data) {
log.warn('Server called client route "render":', data);
var escapedPath = EscapeStringForJSX(data.folder_url);
return runEvalScript("render('" + escapedPath +"', " + data.comp_id + ")")
.then(function(result){
log.warn("render: " + result);
return result;
});
});
RPC.addRoute('AfterEffects.get_extension_version', function (data) {
log.warn('Server called client route "get_extension_version":', data);
return get_extension_version();
});
RPC.addRoute('AfterEffects.get_app_version', function (data) {
log.warn('Server called client route "get_app_version":', data);
return runEvalScript("getAppVersion()")
.then(function(result){
log.warn("get_app_version: " + result);
return result;
});
});
RPC.addRoute('AfterEffects.add_placeholder', function (data) {
log.warn('Server called client route "add_placeholder":', data);
var escapedName = EscapeStringForJSX(data.name);
return runEvalScript("addPlaceholder('" + escapedName +"',"+
data.width + ',' +
data.height + ',' +
data.fps + ',' +
data.duration + ")")
.then(function(result){
log.warn("add_placeholder: " + result);
return result;
});
});
RPC.addRoute('AfterEffects.close', function (data) {
log.warn('Server called client route "close":', data);
return runEvalScript("close()");
});
RPC.addRoute('AfterEffects.print_msg', function (data) {
log.warn('Server called client route "print_msg":', data);
var escaped_msg = EscapeStringForJSX(data.msg);
return runEvalScript("printMsg('" + escaped_msg +"')")
.then(function(result){
log.warn("print_msg: " + result);
return result;
});
});
}
/** main entry point **/
startUp("WEBSOCKET_URL");
(function () {
'use strict';
var csInterface = new CSInterface();
function init() {
themeManager.init();
$("#btn_test").click(function () {
csInterface.evalScript('sayHello()');
});
}
init();
}());
function EscapeStringForJSX(str){
// Replaces:
// \ with \\
// ' with \'
// " with \"
// See: https://stackoverflow.com/a/3967927/5285364
return str.replace(/\\/g, '\\\\').replace(/'/g, "\\'").replace(/"/g,'\\"');
}
function runEvalScript(script) {
// because of asynchronous nature of functions in jsx
// this waits for response
return new Promise(function(resolve, reject){
csInterface.evalScript(script, resolve);
});
}
================================================
FILE: openpype/hosts/aftereffects/api/extension/js/themeManager.js
================================================
/*jslint vars: true, plusplus: true, devel: true, nomen: true, regexp: true, indent: 4, maxerr: 50 */
/*global window, document, CSInterface*/
/*
Responsible for overwriting CSS at runtime according to CC app
settings as defined by the end user.
*/
var themeManager = (function () {
'use strict';
/**
* Convert the Color object to string in hexadecimal format;
*/
function toHex(color, delta) {
function computeValue(value, delta) {
var computedValue = !isNaN(delta) ? value + delta : value;
if (computedValue < 0) {
computedValue = 0;
} else if (computedValue > 255) {
computedValue = 255;
}
computedValue = Math.floor(computedValue);
computedValue = computedValue.toString(16);
return computedValue.length === 1 ? "0" + computedValue : computedValue;
}
var hex = "";
if (color) {
hex = computeValue(color.red, delta) + computeValue(color.green, delta) + computeValue(color.blue, delta);
}
return hex;
}
function reverseColor(color, delta) {
return toHex({
red: Math.abs(255 - color.red),
green: Math.abs(255 - color.green),
blue: Math.abs(255 - color.blue)
},
delta);
}
function addRule(stylesheetId, selector, rule) {
var stylesheet = document.getElementById(stylesheetId);
if (stylesheet) {
stylesheet = stylesheet.sheet;
if (stylesheet.addRule) {
stylesheet.addRule(selector, rule);
} else if (stylesheet.insertRule) {
stylesheet.insertRule(selector + ' { ' + rule + ' }', stylesheet.cssRules.length);
}
}
}
/**
* Update the theme with the AppSkinInfo retrieved from the host product.
*/
function updateThemeWithAppSkinInfo(appSkinInfo) {
var panelBgColor = appSkinInfo.panelBackgroundColor.color;
var bgdColor = toHex(panelBgColor);
var darkBgdColor = toHex(panelBgColor, 20);
var fontColor = "F0F0F0";
if (panelBgColor.red > 122) {
fontColor = "000000";
}
var lightBgdColor = toHex(panelBgColor, -100);
var styleId = "hostStyle";
addRule(styleId, ".hostElt", "background-color:" + "#" + bgdColor);
addRule(styleId, ".hostElt", "font-size:" + appSkinInfo.baseFontSize + "px;");
addRule(styleId, ".hostElt", "font-family:" + appSkinInfo.baseFontFamily);
addRule(styleId, ".hostElt", "color:" + "#" + fontColor);
addRule(styleId, ".hostBgd", "background-color:" + "#" + bgdColor);
addRule(styleId, ".hostBgdDark", "background-color: " + "#" + darkBgdColor);
addRule(styleId, ".hostBgdLight", "background-color: " + "#" + lightBgdColor);
addRule(styleId, ".hostFontSize", "font-size:" + appSkinInfo.baseFontSize + "px;");
addRule(styleId, ".hostFontFamily", "font-family:" + appSkinInfo.baseFontFamily);
addRule(styleId, ".hostFontColor", "color:" + "#" + fontColor);
addRule(styleId, ".hostFont", "font-size:" + appSkinInfo.baseFontSize + "px;");
addRule(styleId, ".hostFont", "font-family:" + appSkinInfo.baseFontFamily);
addRule(styleId, ".hostFont", "color:" + "#" + fontColor);
addRule(styleId, ".hostButton", "background-color:" + "#" + darkBgdColor);
addRule(styleId, ".hostButton:hover", "background-color:" + "#" + bgdColor);
addRule(styleId, ".hostButton:active", "background-color:" + "#" + darkBgdColor);
addRule(styleId, ".hostButton", "border-color: " + "#" + lightBgdColor);
}
function onAppThemeColorChanged(event) {
var skinInfo = JSON.parse(window.__adobe_cep__.getHostEnvironment()).appSkinInfo;
updateThemeWithAppSkinInfo(skinInfo);
}
function init() {
var csInterface = new CSInterface();
updateThemeWithAppSkinInfo(csInterface.hostEnvironment.appSkinInfo);
csInterface.addEventListener(CSInterface.THEME_COLOR_CHANGED_EVENT, onAppThemeColorChanged);
}
return {
init: init
};
}());
================================================
FILE: openpype/hosts/aftereffects/api/extension/jsx/hostscript.jsx
================================================
/*jslint vars: true, plusplus: true, devel: true, nomen: true, regexp: true,
indent: 4, maxerr: 50 */
/*global $, Folder*/
//@include "../js/libs/json.js"
/* All public API function should return JSON! */
app.preferences.savePrefAsBool("General Section", "Show Welcome Screen", false) ;
if(!Array.prototype.indexOf) {
Array.prototype.indexOf = function ( item ) {
var index = 0, length = this.length;
for ( ; index < length; index++ ) {
if ( this[index] === item )
return index;
}
return -1;
};
}
function sayHello(){
alert("hello from ExtendScript");
}
function getEnv(variable){
return $.getenv(variable);
}
function getMetadata(){
/**
* Returns payload in 'Label' field of project's metadata
*
**/
if (ExternalObject.AdobeXMPScript === undefined){
ExternalObject.AdobeXMPScript =
new ExternalObject('lib:AdobeXMPScript');
}
var proj = app.project;
var meta = new XMPMeta(app.project.xmpPacket);
var schemaNS = XMPMeta.getNamespaceURI("xmp");
var label = "xmp:Label";
if (meta.doesPropertyExist(schemaNS, label)){
var prop = meta.getProperty(schemaNS, label);
return prop.value;
}
return _prepareSingleValue([]);
}
function imprint(payload){
/**
* Stores payload in 'Label' field of project's metadata
*
* Args:
* payload (string): json content
*/
if (ExternalObject.AdobeXMPScript === undefined){
ExternalObject.AdobeXMPScript =
new ExternalObject('lib:AdobeXMPScript');
}
var proj = app.project;
var meta = new XMPMeta(app.project.xmpPacket);
var schemaNS = XMPMeta.getNamespaceURI("xmp");
var label = "xmp:Label";
meta.setProperty(schemaNS, label, payload);
app.project.xmpPacket = meta.serialize();
}
function fileOpen(path){
/**
* Opens (project) file on 'path'
*/
fp = new File(path);
return _prepareSingleValue(app.open(fp))
}
function getActiveDocumentName(){
/**
* Returns file name of active document
* */
var file = app.project.file;
if (file){
return _prepareSingleValue(file.name)
}
return _prepareError("No file open currently");
}
function getActiveDocumentFullName(){
/**
* Returns absolute path to current project
* */
var file = app.project.file;
if (file){
var f = new File(file.fullName);
var path = f.fsName;
f.close();
return _prepareSingleValue(path)
}
return _prepareError("No file open currently");
}
function addItem(name, item_type){
/**
* Adds comp or folder to project items.
*
* Could be called when creating publishable instance to prepare
* composition (and render queue).
*
* Args:
* name (str): composition name
* item_type (str): COMP|FOLDER
* Returns:
* SingleItemValue: eg {"result": VALUE}
*/
if (item_type == "COMP"){
// dummy values, will be rewritten later
item = app.project.items.addComp(name, 1920, 1060, 1, 10, 25);
}else if (item_type == "FOLDER"){
item = app.project.items.addFolder(name);
}else{
return _prepareError("Only 'COMP' or 'FOLDER' can be created");
}
return _prepareSingleValue(item.id);
}
function getItems(comps, folders, footages){
/**
* Returns JSON representation of compositions and
* if 'collectLayers' then layers in comps too.
*
* Args:
* comps (bool): return selected compositions
* folders (bool): return folders
* footages (bool): return FootageItem
* Returns:
* (list) of JSON items
*/
var items = []
for (i = 1; i <= app.project.items.length; ++i){
var item = app.project.items[i];
if (!item){
continue;
}
var ret = _getItem(item, comps, folders, footages);
if (ret){
items.push(ret);
}
}
return '[' + items.join() + ']';
}
function selectItems(items){
/**
* Select all items from `items`, deselect other.
*
* Args:
* items (list)
*/
for (i = 1; i <= app.project.items.length; ++i){
item = app.project.items[i];
if (items.indexOf(item.id) > -1){
item.selected = true;
}else{
item.selected = false;
}
}
}
function getSelectedItems(comps, folders, footages){
/**
* Returns list of selected items from Project menu
*
* Args:
* comps (bool): return selected compositions
* folders (bool): return folders
* footages (bool): return FootageItem
* Returns:
* (list) of JSON items
*/
var items = []
for (i = 0; i < app.project.selection.length; ++i){
var item = app.project.selection[i];
if (!item){
continue;
}
var ret = _getItem(item, comps, folders, footages);
if (ret){
items.push(ret);
}
}
return '[' + items.join() + ']';
}
function _getItem(item, comps, folders, footages){
/**
* Auxiliary function as project items and selections
* are indexed in different way :/
* Refactor
*/
var item_type = '';
var path = '';
var containing_comps = [];
if (item instanceof FolderItem){
item_type = 'folder';
if (!folders){
return "{}";
}
}
if (item instanceof FootageItem){
if (!footages){
return "{}";
}
item_type = 'footage';
if (item.file){
path = item.file.fsName;
}
if (item.usedIn){
for (j = 0; j < item.usedIn.length; ++j){
containing_comps.push(item.usedIn[j].id);
}
}
}
if (item instanceof CompItem){
item_type = 'comp';
if (!comps){
return "{}";
}
}
var item = {"name": item.name,
"id": item.id,
"type": item_type,
"path": path,
"containing_comps": containing_comps};
return JSON.stringify(item);
}
function importFile(path, item_name, import_options){
/**
* Imports file (image tested for now) as a FootageItem.
* Creates new composition
*
* Args:
* path (string): absolute path to image file
* item_name (string): label for composition
* Returns:
* JSON {name, id}
*/
var comp;
var ret = {};
try{
import_options = JSON.parse(import_options);
} catch (e){
return _prepareError("Couldn't parse import options " + import_options);
}
app.beginUndoGroup("Import File");
fp = new File(path);
if (fp.exists){
try {
im_opt = new ImportOptions(fp);
importAsType = import_options["ImportAsType"];
if ('ImportAsType' in import_options){ // refactor
if (importAsType.indexOf('COMP') > 0){
im_opt.importAs = ImportAsType.COMP;
}
if (importAsType.indexOf('FOOTAGE') > 0){
im_opt.importAs = ImportAsType.FOOTAGE;
}
if (importAsType.indexOf('COMP_CROPPED_LAYERS') > 0){
im_opt.importAs = ImportAsType.COMP_CROPPED_LAYERS;
}
if (importAsType.indexOf('PROJECT') > 0){
im_opt.importAs = ImportAsType.PROJECT;
}
}
if ('sequence' in import_options){
im_opt.sequence = true;
}
comp = app.project.importFile(im_opt);
if (app.project.selection.length == 2 &&
app.project.selection[0] instanceof FolderItem){
comp.parentFolder = app.project.selection[0]
}
} catch (error) {
return _prepareError(error.toString() + importOptions.file.fsName);
} finally {
fp.close();
}
}else{
return _prepareError("File " + path + " not found.");
}
if (comp){
comp.name = item_name;
comp.label = 9; // Green
ret = {"name": comp.name, "id": comp.id}
}
app.endUndoGroup();
return JSON.stringify(ret);
}
function setLabelColor(comp_id, color_idx){
/**
* Set item_id label to 'color_idx' color
* Args:
* item_id (int): item id
* color_idx (int): 0-16 index from Label
*/
var item = app.project.itemByID(comp_id);
if (item){
item.label = color_idx;
}else{
return _prepareError("There is no composition with "+ comp_id);
}
}
function replaceItem(item_id, path, item_name){
/**
* Replaces loaded file with new file and updates name
*
* Args:
* item_id (int): id of composition, not a index!
* path (string): absolute path to new file
* item_name (string): new composition name
*/
app.beginUndoGroup("Replace File");
fp = new File(path);
if (!fp.exists){
return _prepareError("File " + path + " not found.");
}
var item = app.project.itemByID(item_id);
if (item){
try{
if (isFileSequence(item)) {
item.replaceWithSequence(fp, false);
}else{
item.replace(fp);
}
item.name = item_name;
} catch (error) {
return _prepareError(error.toString() + path);
} finally {
fp.close();
}
}else{
return _prepareError("There is no item with "+ item_id);
}
app.endUndoGroup();
}
function renameItem(item_id, new_name){
/**
* Renames item with 'item_id' to 'new_name'
*
* Args:
* item_id (int): id to search item
* new_name (str)
*/
var item = app.project.itemByID(item_id);
if (item){
item.name = new_name;
}else{
return _prepareError("There is no composition with "+ comp_id);
}
}
function deleteItem(item_id){
/**
* Delete any 'item_id'
*
* Not restricted only to comp, it could delete
* any item with 'id'
*/
var item = app.project.itemByID(item_id);
if (item){
item.remove();
}else{
return _prepareError("There is no composition with "+ comp_id);
}
}
function getCompProperties(comp_id){
/**
* Returns information about composition - are that will be
* rendered.
*
* Returns
* (dict)
*/
var comp = app.project.itemByID(comp_id);
if (!comp){
return _prepareError("There is no composition with "+ comp_id);
}
return JSON.stringify({
"id": comp.id,
"name": comp.name,
"frameStart": comp.displayStartFrame,
"framesDuration": comp.duration * comp.frameRate,
"frameRate": comp.frameRate,
"width": comp.width,
"height": comp.height});
}
function setCompProperties(comp_id, frameStart, framesCount, frameRate,
width, height){
/**
* Sets work area info from outside (from Ftrack via OpenPype)
*/
var comp = app.project.itemByID(comp_id);
if (!comp){
return _prepareError("There is no composition with "+ comp_id);
}
app.beginUndoGroup('change comp properties');
if (frameStart && framesCount && frameRate){
comp.displayStartFrame = frameStart;
comp.duration = framesCount / frameRate;
comp.frameRate = frameRate;
}
if (width && height){
var widthOld = comp.width;
var widthNew = width;
var widthDelta = widthNew - widthOld;
var heightOld = comp.height;
var heightNew = height;
var heightDelta = heightNew - heightOld;
var offset = [widthDelta / 2, heightDelta / 2];
comp.width = widthNew;
comp.height = heightNew;
for (var i = 1, il = comp.numLayers; i <= il; i++) {
var layer = comp.layer(i);
var positionProperty = layer.property('ADBE Transform Group').property('ADBE Position');
if (positionProperty.numKeys > 0) {
for (var j = 1, jl = positionProperty.numKeys; j <= jl; j++) {
var keyValue = positionProperty.keyValue(j);
positionProperty.setValueAtKey(j, keyValue + offset);
}
} else {
var positionValue = positionProperty.value;
positionProperty.setValue(positionValue + offset);
}
}
}
app.endUndoGroup();
}
function save(){
/**
* Saves current project
*/
app.project.save(); //TODO path is wrong, File instead
}
function saveAs(path){
/**
* Saves current project as 'path'
* */
app.project.save(fp = new File(path));
}
function getRenderInfo(comp_id){
/***
Get info from render queue.
Currently pulls only file name to parse extension and
if it is sequence in Python
Args:
comp_id (int): id of composition
Return:
(list) [{file_name:"xx.png", width:00, height:00}]
**/
var item = app.project.itemByID(comp_id);
if (!item){
return _prepareError("Composition with '" + comp_id + "' wasn't found! Recreate publishable instance(s)")
}
var comp_name = item.name;
var output_metadata = []
try{
// render_item.duplicate() should create new item on renderQueue
// BUT it works only sometimes, there are some weird synchronization issue
// this method will be called always before render, so prepare items here
// for render to spare the hassle
for (i = 1; i <= app.project.renderQueue.numItems; ++i){
var render_item = app.project.renderQueue.item(i);
if (render_item.comp.id != comp_id){
continue;
}
if (render_item.status == RQItemStatus.DONE){
render_item.duplicate(); // create new, cannot change status if DONE
render_item.remove(); // remove existing to limit duplications
continue;
}
}
// properly validate as `numItems` won't change magically
var comp_id_count = 0;
for (i = 1; i <= app.project.renderQueue.numItems; ++i){
var render_item = app.project.renderQueue.item(i);
if (render_item.comp.id != comp_id){
continue;
}
comp_id_count += 1;
var item = render_item.outputModule(1);
for (j = 1; j<= render_item.numOutputModules; ++j){
var file_url = item.file.toString();
output_metadata.push(
JSON.stringify({
"file_name": file_url,
"width": render_item.comp.width,
"height": render_item.comp.height
})
);
}
}
} catch (error) {
return _prepareError("There is no render queue, create one");
}
if (comp_id_count > 1){
return _prepareError("There cannot be more items in Render Queue for '" + comp_name + "'!")
}
if (comp_id_count == 0){
return _prepareError("There is no item in Render Queue for '" + comp_name + "'! Add composition to Render Queue.")
}
return '[' + output_metadata.join() + ']';
}
function getAudioUrlForComp(comp_id){
/**
* Searches composition for audio layer
*
* Only single AVLayer is expected!
* Used for collecting Audio
*
* Args:
* comp_id (int): id of composition
* Return:
* (str) with url to audio content
*/
var item = app.project.itemByID(comp_id);
if (item){
for (i = 1; i <= item.numLayers; ++i){
var layer = item.layers[i];
if (layer instanceof AVLayer){
if (layer.hasAudio){
source_url = layer.source.file.fsName.toString()
return _prepareSingleValue(source_url);
}
}
}
}else{
return _prepareError("There is no composition with "+ comp_id);
}
}
function addItemAsLayerToComp(comp_id, item_id, found_comp){
/**
* Adds already imported FootageItem ('item_id') as a new
* layer to composition ('comp_id').
*
* Args:
* comp_id (int): id of target composition
* item_id (int): FootageItem.id
* found_comp (CompItem, optional): to limit quering if
* comp already found previously
*/
var comp = found_comp || app.project.itemByID(comp_id);
if (comp){
item = app.project.itemByID(item_id);
if (item){
comp.layers.add(item);
}else{
return _prepareError("There is no item with " + item_id);
}
}else{
return _prepareError("There is no composition with "+ comp_id);
}
}
function importBackground(comp_id, composition_name, files_to_import){
/**
* Imports backgrounds images to existing or new composition.
*
* If comp_id is not provided, new composition is created, basic
* values (width, heights, frameRatio) takes from first imported
* image.
*
* Args:
* comp_id (int): id of existing composition (null if new)
* composition_name (str): used when new composition
* files_to_import (list): list of absolute paths to import and
* add as layers
*
* Returns:
* (str): json representation (id, name, members)
*/
var comp;
var folder;
var imported_ids = [];
if (comp_id){
comp = app.project.itemByID(comp_id);
folder = comp.parentFolder;
}else{
if (app.project.selection.length > 1){
return _prepareError(
"Too many items selected, select only target composition!");
}else{
selected_item = app.project.activeItem;
if (selected_item instanceof Folder){
comp = selected_item;
folder = selected_item;
}
}
}
if (files_to_import){
for (i = 0; i < files_to_import.length; ++i){
item = _importItem(files_to_import[i]);
if (!item){
return _prepareError(
"No item for " + item_json["id"] +
". Import background failed.")
}
if (!comp){
folder = app.project.items.addFolder(composition_name);
imported_ids.push(folder.id);
comp = app.project.items.addComp(composition_name, item.width,
item.height, item.pixelAspect,
1, 26.7); // hardcode defaults
imported_ids.push(comp.id);
comp.parentFolder = folder;
}
imported_ids.push(item.id)
item.parentFolder = folder;
addItemAsLayerToComp(comp.id, item.id, comp);
}
}
var item = {"name": comp.name,
"id": folder.id,
"members": imported_ids};
return JSON.stringify(item);
}
function reloadBackground(comp_id, composition_name, files_to_import){
/**
* Reloads existing composition.
*
* It deletes complete composition with encompassing folder, recreates
* from scratch via 'importBackground' functionality.
*
* Args:
* comp_id (int): id of existing composition (null if new)
* composition_name (str): used when new composition
* files_to_import (list): list of absolute paths to import and
* add as layers
*
* Returns:
* (str): json representation (id, name, members)
*
*/
var imported_ids = []; // keep track of members of composition
comp = app.project.itemByID(comp_id);
folder = comp.parentFolder;
if (folder){
renameItem(folder.id, composition_name);
imported_ids.push(folder.id);
}
if (comp){
renameItem(comp.id, composition_name);
imported_ids.push(comp.id);
}
var existing_layer_names = [];
var existing_layer_ids = []; // because ExtendedScript doesnt have keys()
for (i = 1; i <= folder.items.length; ++i){
layer = folder.items[i];
//because comp.layers[i] doesnt have 'id' accessible
if (layer instanceof CompItem){
continue;
}
existing_layer_names.push(layer.name);
existing_layer_ids.push(layer.id);
}
var new_filenames = [];
if (files_to_import){
for (i = 0; i < files_to_import.length; ++i){
file_name = _get_file_name(files_to_import[i]);
new_filenames.push(file_name);
idx = existing_layer_names.indexOf(file_name);
if (idx >= 0){ // update
var layer_id = existing_layer_ids[idx];
replaceItem(layer_id, files_to_import[i], file_name);
imported_ids.push(layer_id);
}else{ // new layer
item = _importItem(files_to_import[i]);
if (!item){
return _prepareError(
"No item for " + files_to_import[i] +
". Reload background failed.");
}
imported_ids.push(item.id);
item.parentFolder = folder;
addItemAsLayerToComp(comp.id, item.id, comp);
}
}
}
_delete_obsolete_items(folder, new_filenames);
var item = {"name": comp.name,
"id": folder.id,
"members": imported_ids};
return JSON.stringify(item);
}
function _get_file_name(file_url){
/**
* Returns file name without extension from 'file_url'
*
* Args:
* file_url (str): full absolute url
* Returns:
* (str)
*/
fp = new File(file_url);
file_name = fp.name.substring(0, fp.name.lastIndexOf("."));
return file_name;
}
function _delete_obsolete_items(folder, new_filenames){
/***
* Goes through 'folder' and removes layers not in new
* background
*
* Args:
* folder (FolderItem)
* new_filenames (array): list of layer names in new bg
*/
// remove items in old, but not in new
delete_ids = []
for (i = 1; i <= folder.items.length; ++i){
layer = folder.items[i];
//because comp.layers[i] doesnt have 'id' accessible
if (layer instanceof CompItem){
continue;
}
if (new_filenames.indexOf(layer.name) < 0){
delete_ids.push(layer.id);
}
}
for (i = 0; i < delete_ids.length; ++i){
deleteItem(delete_ids[i]);
}
}
function _importItem(file_url){
/**
* Imports 'file_url' as new FootageItem
*
* Args:
* file_url (str): file url with content
* Returns:
* (FootageItem)
*/
file_name = _get_file_name(file_url);
//importFile prepared previously to return json
item_json = importFile(file_url, file_name, JSON.stringify({"ImportAsType":"FOOTAGE"}));
item_json = JSON.parse(item_json);
item = app.project.itemByID(item_json["id"]);
return item;
}
function isFileSequence (item){
/**
* Check that item is a recognizable sequence
*/
if (item instanceof FootageItem && item.mainSource instanceof FileSource && !(item.mainSource.isStill) && item.hasVideo){
var extname = item.mainSource.file.fsName.split('.').pop();
return extname.match(new RegExp("(ai|bmp|bw|cin|cr2|crw|dcr|dng|dib|dpx|eps|erf|exr|gif|hdr|ico|icb|iff|jpe|jpeg|jpg|mos|mrw|nef|orf|pbm|pef|pct|pcx|pdf|pic|pict|png|ps|psd|pxr|raf|raw|rgb|rgbe|rla|rle|rpf|sgi|srf|tdi|tga|tif|tiff|vda|vst|x3f|xyze)", "i")) !== null;
}
return false;
}
function render(target_folder, comp_id){
var out_dir = new Folder(target_folder);
var out_dir = out_dir.fsName;
for (i = 1; i <= app.project.renderQueue.numItems; ++i){
var render_item = app.project.renderQueue.item(i);
var composition = render_item.comp;
if (composition.id == comp_id){
if (render_item.status == RQItemStatus.DONE){
var new_item = render_item.duplicate();
render_item.remove();
render_item = new_item;
}
render_item.render = true;
var om1 = app.project.renderQueue.item(i).outputModule(1);
var file_name = File.decode( om1.file.name ).replace('℗', ''); // Name contains special character, space?
var omItem1_settable_str = app.project.renderQueue.item(i).outputModule(1).getSettings( GetSettingsFormat.STRING_SETTABLE );
var targetFolder = new Folder(target_folder);
if (!targetFolder.exists) {
targetFolder.create();
}
om1.file = new File(targetFolder.fsName + '/' + file_name);
}else{
if (render_item.status != RQItemStatus.DONE){
render_item.render = false;
}
}
}
app.beginSuppressDialogs();
app.project.renderQueue.render();
app.endSuppressDialogs(false);
}
function close(){
app.project.close(CloseOptions.DO_NOT_SAVE_CHANGES);
app.quit();
}
function getAppVersion(){
return _prepareSingleValue(app.version);
}
function printMsg(msg){
alert(msg);
}
function addPlaceholder(name, width, height, fps, duration){
/** Add AE PlaceholderItem to Project list.
*
* PlaceholderItem chosen as it doesn't require existing file and
* might potentially allow nice functionality in the future.
*
*/
app.beginUndoGroup('change comp properties');
try{
item = app.project.importPlaceholder(name, width, height,
fps, duration);
return _prepareSingleValue(item.id);
}catch (error) {
writeLn(_prepareError("Cannot add placeholder " + error.toString()));
}
app.endUndoGroup();
}
function addItemInstead(placeholder_item_id, item_id){
/** Add new loaded item in place of load placeholder.
*
* Each placeholder could be placed multiple times into multiple
* composition. This loops through all compositions and
* places loaded item under placeholder.
* Placeholder item gets deleted later separately according
* to configuration in Settings.
*
* Args:
* placeholder_item_id (int)
* item_id (int)
*/
var item = app.project.itemByID(item_id);
if (!item){
return _prepareError("There is no item with "+ item_id);
}
app.beginUndoGroup('Add loaded items');
for (i = 1; i <= app.project.items.length; ++i){
var comp = app.project.items[i];
if (!(comp instanceof CompItem)){
continue
}
var i = 1;
while (i <= comp.numLayers) {
var layer = comp.layer(i);
var layer_source = layer.source;
if (layer_source && layer_source.id == placeholder_item_id){
var new_layer = comp.layers.add(item);
new_layer.moveAfter(layer);
// copy all(?) properties to new layer
layer.property("ADBE Transform Group").copyToComp(new_layer);
i = i + 1;
}
i = i + 1;
}
}
app.endUndoGroup();
}
function _prepareSingleValue(value){
return JSON.stringify({"result": value})
}
function _prepareError(error_msg){
return JSON.stringify({"error": error_msg})
}
================================================
FILE: openpype/hosts/aftereffects/api/launch_logic.py
================================================
import os
import sys
import subprocess
import collections
import logging
import asyncio
import functools
import traceback
from wsrpc_aiohttp import (
WebSocketRoute,
WebSocketAsync
)
from qtpy import QtCore
from openpype.lib import Logger
from openpype.tests.lib import is_in_tests
from openpype.pipeline import install_host, legacy_io
from openpype.modules import ModulesManager
from openpype.tools.utils import host_tools, get_openpype_qt_app
from openpype.tools.adobe_webserver.app import WebServerTool
from .ws_stub import get_stub
from .lib import set_settings
log = logging.getLogger(__name__)
log.setLevel(logging.DEBUG)
def safe_excepthook(*args):
traceback.print_exception(*args)
def main(*subprocess_args):
"""Main entrypoint to AE launching, called from pre hook."""
sys.excepthook = safe_excepthook
from openpype.hosts.aftereffects.api import AfterEffectsHost
host = AfterEffectsHost()
install_host(host)
os.environ["OPENPYPE_LOG_NO_COLORS"] = "False"
app = get_openpype_qt_app()
app.setQuitOnLastWindowClosed(False)
launcher = ProcessLauncher(subprocess_args)
launcher.start()
if os.environ.get("HEADLESS_PUBLISH"):
manager = ModulesManager()
webpublisher_addon = manager["webpublisher"]
launcher.execute_in_main_thread(
functools.partial(
webpublisher_addon.headless_publish,
log,
"CloseAE",
is_in_tests()
)
)
elif os.environ.get("AVALON_PHOTOSHOP_WORKFILES_ON_LAUNCH", True):
save = False
if os.getenv("WORKFILES_SAVE_AS"):
save = True
launcher.execute_in_main_thread(
lambda: host_tools.show_tool_by_name("workfiles", save=save)
)
sys.exit(app.exec_())
def show_tool_by_name(tool_name):
kwargs = {}
if tool_name == "loader":
kwargs["use_context"] = True
host_tools.show_tool_by_name(tool_name, **kwargs)
class ProcessLauncher(QtCore.QObject):
"""Launches webserver, connects to it, runs main thread."""
route_name = "AfterEffects"
_main_thread_callbacks = collections.deque()
def __init__(self, subprocess_args):
self._subprocess_args = subprocess_args
self._log = None
super(ProcessLauncher, self).__init__()
# Keep track if launcher was alreadu started
self._started = False
self._process = None
self._websocket_server = None
start_process_timer = QtCore.QTimer()
start_process_timer.setInterval(100)
loop_timer = QtCore.QTimer()
loop_timer.setInterval(200)
start_process_timer.timeout.connect(self._on_start_process_timer)
loop_timer.timeout.connect(self._on_loop_timer)
self._start_process_timer = start_process_timer
self._loop_timer = loop_timer
@property
def log(self):
if self._log is None:
self._log = Logger.get_logger("{}-launcher".format(
self.route_name))
return self._log
@property
def websocket_server_is_running(self):
if self._websocket_server is not None:
return self._websocket_server.is_running
return False
@property
def is_process_running(self):
if self._process is not None:
return self._process.poll() is None
return False
@property
def is_host_connected(self):
"""Returns True if connected, False if app is not running at all."""
if not self.is_process_running:
return False
try:
_stub = get_stub()
if _stub:
return True
except Exception:
pass
return None
@classmethod
def execute_in_main_thread(cls, callback):
cls._main_thread_callbacks.append(callback)
def start(self):
if self._started:
return
self.log.info("Started launch logic of AfterEffects")
self._started = True
self._start_process_timer.start()
def exit(self):
""" Exit whole application. """
if self._start_process_timer.isActive():
self._start_process_timer.stop()
if self._loop_timer.isActive():
self._loop_timer.stop()
if self._websocket_server is not None:
self._websocket_server.stop()
if self._process:
self._process.kill()
self._process.wait()
QtCore.QCoreApplication.exit()
def _on_loop_timer(self):
# TODO find better way and catch errors
# Run only callbacks that are in queue at the moment
cls = self.__class__
for _ in range(len(cls._main_thread_callbacks)):
if cls._main_thread_callbacks:
callback = cls._main_thread_callbacks.popleft()
callback()
if not self.is_process_running:
self.log.info("Host process is not running. Closing")
self.exit()
elif not self.websocket_server_is_running:
self.log.info("Websocket server is not running. Closing")
self.exit()
def _on_start_process_timer(self):
# TODO add try except validations for each part in this method
# Start server as first thing
if self._websocket_server is None:
self._init_server()
return
# TODO add waiting time
# Wait for webserver
if not self.websocket_server_is_running:
return
# Start application process
if self._process is None:
self._start_process()
self.log.info("Waiting for host to connect")
return
# TODO add waiting time
# Wait until host is connected
if self.is_host_connected:
self._start_process_timer.stop()
self._loop_timer.start()
elif (
not self.is_process_running
or not self.websocket_server_is_running
):
self.exit()
def _init_server(self):
if self._websocket_server is not None:
return
self.log.debug(
"Initialization of websocket server for host communication"
)
self._websocket_server = websocket_server = WebServerTool()
if websocket_server.port_occupied(
websocket_server.host_name,
websocket_server.port
):
self.log.info(
"Server already running, sending actual context and exit."
)
asyncio.run(websocket_server.send_context_change(self.route_name))
self.exit()
return
# Add Websocket route
websocket_server.add_route("*", "/ws/", WebSocketAsync)
# Add after effects route to websocket handler
print("Adding {} route".format(self.route_name))
WebSocketAsync.add_route(
self.route_name, AfterEffectsRoute
)
self.log.info("Starting websocket server for host communication")
websocket_server.start_server()
def _start_process(self):
if self._process is not None:
return
self.log.info("Starting host process")
try:
self._process = subprocess.Popen(
self._subprocess_args,
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL
)
except Exception:
self.log.info("exce", exc_info=True)
self.exit()
class AfterEffectsRoute(WebSocketRoute):
"""
One route, mimicking external application (like Harmony, etc).
All functions could be called from client.
'do_notify' function calls function on the client - mimicking
notification after long running job on the server or similar
"""
instance = None
def init(self, **kwargs):
# Python __init__ must be return "self".
# This method might return anything.
log.debug("someone called AfterEffects route")
self.instance = self
return kwargs
# server functions
async def ping(self):
log.debug("someone called AfterEffects route ping")
# This method calls function on the client side
# client functions
async def set_context(self, project, asset, task):
"""
Sets 'project' and 'asset' to envs, eg. setting context
Args:
project (str)
asset (str)
"""
log.info("Setting context change")
log.info("project {} asset {} ".format(project, asset))
if project:
legacy_io.Session["AVALON_PROJECT"] = project
os.environ["AVALON_PROJECT"] = project
if asset:
legacy_io.Session["AVALON_ASSET"] = asset
os.environ["AVALON_ASSET"] = asset
if task:
legacy_io.Session["AVALON_TASK"] = task
os.environ["AVALON_TASK"] = task
async def read(self):
log.debug("aftereffects.read client calls server server calls "
"aftereffects client")
return await self.socket.call('aftereffects.read')
# panel routes for tools
async def workfiles_route(self):
self._tool_route("workfiles")
async def loader_route(self):
self._tool_route("loader")
async def publish_route(self):
self._tool_route("publisher")
async def sceneinventory_route(self):
self._tool_route("sceneinventory")
async def setresolution_route(self):
self._settings_route(False, True)
async def setframes_route(self):
self._settings_route(True, False)
async def setall_route(self):
self._settings_route(True, True)
async def experimental_tools_route(self):
self._tool_route("experimental_tools")
def _tool_route(self, _tool_name):
"""The address accessed when clicking on the buttons."""
partial_method = functools.partial(show_tool_by_name,
_tool_name)
ProcessLauncher.execute_in_main_thread(partial_method)
# Required return statement.
return "nothing"
def _settings_route(self, frames, resolution):
partial_method = functools.partial(set_settings,
frames,
resolution)
ProcessLauncher.execute_in_main_thread(partial_method)
# Required return statement.
return "nothing"
def create_placeholder_route(self):
from openpype.hosts.aftereffects.api.workfile_template_builder import \
create_placeholder
partial_method = functools.partial(create_placeholder)
ProcessLauncher.execute_in_main_thread(partial_method)
# Required return statement.
return "nothing"
def update_placeholder_route(self):
from openpype.hosts.aftereffects.api.workfile_template_builder import \
update_placeholder
partial_method = functools.partial(update_placeholder)
ProcessLauncher.execute_in_main_thread(partial_method)
# Required return statement.
return "nothing"
def build_workfile_template_route(self):
from openpype.hosts.aftereffects.api.workfile_template_builder import \
build_workfile_template
partial_method = functools.partial(build_workfile_template)
ProcessLauncher.execute_in_main_thread(partial_method)
# Required return statement.
return "nothing"
================================================
FILE: openpype/hosts/aftereffects/api/lib.py
================================================
import os
import re
import json
import contextlib
import logging
from openpype.pipeline.context_tools import get_current_context
from openpype.client import get_asset_by_name
from .ws_stub import get_stub
log = logging.getLogger(__name__)
log.setLevel(logging.DEBUG)
@contextlib.contextmanager
def maintained_selection():
"""Maintain selection during context."""
selection = get_stub().get_selected_items(True, False, False)
try:
yield selection
finally:
pass
def get_extension_manifest_path():
return os.path.join(
os.path.dirname(os.path.abspath(__file__)),
"extension",
"CSXS",
"manifest.xml"
)
def get_unique_layer_name(layers, name):
"""
Gets all layer names and if 'name' is present in them, increases
suffix by 1 (eg. creates unique layer name - for Loader)
Args:
layers (list): of strings, names only
name (string): checked value
Returns:
(string): name_00X (without version)
"""
names = {}
for layer in layers:
layer_name = re.sub(r'_\d{3}$', '', layer)
if layer_name in names.keys():
names[layer_name] = names[layer_name] + 1
else:
names[layer_name] = 1
occurrences = names.get(name, 0)
return "{}_{:0>3d}".format(name, occurrences + 1)
def get_background_layers(file_url):
"""
Pulls file name from background json file, enrich with folder url for
AE to be able import files.
Order is important, follows order in json.
Args:
file_url (str): abs url of background json
Returns:
(list): of abs paths to images
"""
with open(file_url) as json_file:
data = json.load(json_file)
layers = list()
bg_folder = os.path.dirname(file_url)
for child in data['children']:
if child.get("filename"):
layers.append(os.path.join(bg_folder, child.get("filename")).
replace("\\", "/"))
else:
for layer in child['children']:
if layer.get("filename"):
layers.append(os.path.join(bg_folder,
layer.get("filename")).
replace("\\", "/"))
return layers
def get_asset_settings(asset_doc):
"""Get settings on current asset from database.
Returns:
dict: Scene data.
"""
asset_data = asset_doc["data"]
fps = asset_data.get("fps", 0)
frame_start = asset_data.get("frameStart", 0)
frame_end = asset_data.get("frameEnd", 0)
handle_start = asset_data.get("handleStart", 0)
handle_end = asset_data.get("handleEnd", 0)
resolution_width = asset_data.get("resolutionWidth", 0)
resolution_height = asset_data.get("resolutionHeight", 0)
duration = (frame_end - frame_start + 1) + handle_start + handle_end
return {
"fps": fps,
"frameStart": frame_start,
"frameEnd": frame_end,
"handleStart": handle_start,
"handleEnd": handle_end,
"resolutionWidth": resolution_width,
"resolutionHeight": resolution_height,
"duration": duration
}
def set_settings(frames, resolution, comp_ids=None, print_msg=True):
"""Sets number of frames and resolution to selected comps.
Args:
frames (bool): True if set frame info
resolution (bool): True if set resolution
comp_ids (list): specific composition ids, if empty
it tries to look for currently selected
print_msg (bool): True throw JS alert with msg
"""
frame_start = frames_duration = fps = width = height = None
current_context = get_current_context()
asset_doc = get_asset_by_name(current_context["project_name"],
current_context["asset_name"])
settings = get_asset_settings(asset_doc)
msg = ''
if frames:
frame_start = settings["frameStart"] - settings["handleStart"]
frames_duration = settings["duration"]
fps = settings["fps"]
msg += f"frame start:{frame_start}, duration:{frames_duration}, "\
f"fps:{fps}"
if resolution:
width = settings["resolutionWidth"]
height = settings["resolutionHeight"]
msg += f"width:{width} and height:{height}"
stub = get_stub()
if not comp_ids:
comps = stub.get_selected_items(True, False, False)
comp_ids = [comp.id for comp in comps]
if not comp_ids:
stub.print_msg("Select at least one composition to apply settings.")
return
for comp_id in comp_ids:
msg = f"Setting for comp {comp_id} " + msg
log.debug(msg)
stub.set_comp_properties(comp_id, frame_start, frames_duration,
fps, width, height)
if print_msg:
stub.print_msg(msg)
================================================
FILE: openpype/hosts/aftereffects/api/pipeline.py
================================================
import os
from qtpy import QtWidgets
import pyblish.api
from openpype.lib import Logger, register_event_callback
from openpype.pipeline import (
register_loader_plugin_path,
register_creator_plugin_path,
AVALON_CONTAINER_ID,
)
from openpype.hosts.aftereffects.api.workfile_template_builder import (
AEPlaceholderLoadPlugin,
AEPlaceholderCreatePlugin
)
from openpype.pipeline.load import any_outdated_containers
import openpype.hosts.aftereffects
from openpype.host import (
HostBase,
IWorkfileHost,
ILoadHost,
IPublishHost
)
from openpype.tools.utils import get_openpype_qt_app
from .launch_logic import get_stub
from .ws_stub import ConnectionNotEstablishedYet
log = Logger.get_logger(__name__)
HOST_DIR = os.path.dirname(
os.path.abspath(openpype.hosts.aftereffects.__file__)
)
PLUGINS_DIR = os.path.join(HOST_DIR, "plugins")
PUBLISH_PATH = os.path.join(PLUGINS_DIR, "publish")
LOAD_PATH = os.path.join(PLUGINS_DIR, "load")
CREATE_PATH = os.path.join(PLUGINS_DIR, "create")
class AfterEffectsHost(HostBase, IWorkfileHost, ILoadHost, IPublishHost):
name = "aftereffects"
def __init__(self):
self._stub = None
super(AfterEffectsHost, self).__init__()
@property
def stub(self):
"""
Handle pulling stub from PS to run operations on host
Returns:
(AEServerStub) or None
"""
if self._stub:
return self._stub
try:
stub = get_stub() # only after Photoshop is up
except ConnectionNotEstablishedYet:
print("Not connected yet, ignoring")
return
self._stub = stub
return self._stub
def install(self):
print("Installing Pype config...")
pyblish.api.register_host("aftereffects")
pyblish.api.register_plugin_path(PUBLISH_PATH)
register_loader_plugin_path(LOAD_PATH)
register_creator_plugin_path(CREATE_PATH)
register_event_callback("application.launched", application_launch)
def get_workfile_extensions(self):
return [".aep"]
def save_workfile(self, dst_path=None):
self.stub.saveAs(dst_path, True)
def open_workfile(self, filepath):
self.stub.open(filepath)
return True
def get_current_workfile(self):
try:
full_name = get_stub().get_active_document_full_name()
if full_name and full_name != "null":
return os.path.normpath(full_name).replace("\\", "/")
except ValueError:
print("Nothing opened")
pass
return None
def get_containers(self):
return ls()
def get_context_data(self):
meta = self.stub.get_metadata()
for item in meta:
if item.get("id") == "publish_context":
item.pop("id")
return item
return {}
def update_context_data(self, data, changes):
item = data
item["id"] = "publish_context"
self.stub.imprint(item["id"], item)
def get_workfile_build_placeholder_plugins(self):
return [
AEPlaceholderLoadPlugin,
AEPlaceholderCreatePlugin
]
# created instances section
def list_instances(self):
"""List all created instances from current workfile which
will be published.
Pulls from File > File Info
For SubsetManager
Returns:
(list) of dictionaries matching instances format
"""
stub = self.stub
if not stub:
return []
instances = []
layers_meta = stub.get_metadata()
for instance in layers_meta:
if instance.get("id") == "pyblish.avalon.instance":
instances.append(instance)
return instances
def remove_instance(self, instance):
"""Remove instance from current workfile metadata.
Updates metadata of current file in File > File Info and removes
icon highlight on group layer.
For SubsetManager
Args:
instance (dict): instance representation from subsetmanager model
"""
stub = self.stub
if not stub:
return
inst_id = instance.get("instance_id") or instance.get("uuid") # legacy
if not inst_id:
log.warning("No instance identifier for {}".format(instance))
return
stub.remove_instance(inst_id)
if instance.get("members"):
item = stub.get_item(instance["members"][0])
if item:
stub.rename_item(item.id,
item.name.replace(stub.PUBLISH_ICON, ''))
def application_launch():
"""Triggered after start of app"""
check_inventory()
def ls():
"""Yields containers from active AfterEffects document.
This is the host-equivalent of api.ls(), but instead of listing
assets on disk, it lists assets already loaded in AE; once loaded
they are called 'containers'. Used in Manage tool.
Containers could be on multiple levels, single images/videos/was as a
FootageItem, or multiple items - backgrounds (folder with automatically
created composition and all imported layers).
Yields:
dict: container
"""
try:
stub = get_stub() # only after AfterEffects is up
except ConnectionNotEstablishedYet:
print("Not connected yet, ignoring")
return
layers_meta = stub.get_metadata()
for item in stub.get_items(comps=True,
folders=True,
footages=True):
data = stub.read(item, layers_meta)
# Skip non-tagged layers.
if not data:
continue
# Filter to only containers.
if "container" not in data["id"]:
continue
# Append transient data
data["objectName"] = item.name.replace(stub.LOADED_ICON, '')
data["layer"] = item
yield data
def check_inventory():
"""Checks loaded containers if they are of highest version"""
if not any_outdated_containers():
return
# Warn about outdated containers.
_app = get_openpype_qt_app()
message_box = QtWidgets.QMessageBox()
message_box.setIcon(QtWidgets.QMessageBox.Warning)
msg = "There are outdated containers in the scene."
message_box.setText(msg)
message_box.exec_()
def containerise(name,
namespace,
comp,
context,
loader=None,
suffix="_CON"):
"""
Containerisation enables a tracking of version, author and origin
for loaded assets.
Creates dictionary payloads that gets saved into file metadata. Each
container contains of who loaded (loader) and members (single or multiple
in case of background).
Arguments:
name (str): Name of resulting assembly
namespace (str): Namespace under which to host container
comp (AEItem): Composition to containerise
context (dict): Asset information
loader (str, optional): Name of loader used to produce this container.
suffix (str, optional): Suffix of container, defaults to `_CON`.
Returns:
container (str): Name of container assembly
"""
data = {
"schema": "openpype:container-2.0",
"id": AVALON_CONTAINER_ID,
"name": name,
"namespace": namespace,
"loader": str(loader),
"representation": str(context["representation"]["_id"]),
"members": comp.members or [comp.id]
}
stub = get_stub()
stub.imprint(comp.id, data)
return comp
def cache_and_get_instances(creator):
"""Cache instances in shared data.
Storing all instances as a list as legacy instances might be still present.
Args:
creator (Creator): Plugin which would like to get instances from host.
Returns:
List[]: list of all instances stored in metadata
"""
shared_key = "openpype.photoshop.instances"
if shared_key not in creator.collection_shared_data:
creator.collection_shared_data[shared_key] = \
creator.host.list_instances()
return creator.collection_shared_data[shared_key]
================================================
FILE: openpype/hosts/aftereffects/api/plugin.py
================================================
import six
from abc import ABCMeta
from openpype.pipeline import LoaderPlugin
from .launch_logic import get_stub
@six.add_metaclass(ABCMeta)
class AfterEffectsLoader(LoaderPlugin):
@staticmethod
def get_stub():
return get_stub()
================================================
FILE: openpype/hosts/aftereffects/api/workfile_template_builder.py
================================================
import os.path
import uuid
import shutil
from openpype.pipeline import registered_host
from openpype.tools.workfile_template_build import (
WorkfileBuildPlaceholderDialog,
)
from openpype.pipeline.workfile.workfile_template_builder import (
AbstractTemplateBuilder,
PlaceholderPlugin,
LoadPlaceholderItem,
CreatePlaceholderItem,
PlaceholderLoadMixin,
PlaceholderCreateMixin
)
from openpype.hosts.aftereffects.api import get_stub
from openpype.hosts.aftereffects.api.lib import set_settings
PLACEHOLDER_SET = "PLACEHOLDERS_SET"
PLACEHOLDER_ID = "openpype.placeholder"
class AETemplateBuilder(AbstractTemplateBuilder):
"""Concrete implementation of AbstractTemplateBuilder for AE"""
def import_template(self, path):
"""Import template into current scene.
Block if a template is already loaded.
Args:
path (str): A path to current template (usually given by
get_template_preset implementation)
Returns:
bool: Whether the template was successfully imported or not
"""
stub = get_stub()
if not os.path.exists(path):
stub.print_msg(f"Template file on {path} doesn't exist.")
return
stub.save()
workfile_path = stub.get_active_document_full_name()
shutil.copy2(path, workfile_path)
stub.open(workfile_path)
return True
class AEPlaceholderPlugin(PlaceholderPlugin):
"""Contains generic methods for all PlaceholderPlugins."""
def collect_placeholders(self):
"""Collect info from file metadata about created placeholders.
Returns:
(list) (LoadPlaceholderItem)
"""
output = []
scene_placeholders = self._collect_scene_placeholders()
for item in scene_placeholders:
if item.get("plugin_identifier") != self.identifier:
continue
if isinstance(self, AEPlaceholderLoadPlugin):
item = LoadPlaceholderItem(item["uuid"],
item["data"],
self)
elif isinstance(self, AEPlaceholderCreatePlugin):
item = CreatePlaceholderItem(item["uuid"],
item["data"],
self)
else:
raise NotImplementedError(f"Not implemented for {type(self)}")
output.append(item)
return output
def update_placeholder(self, placeholder_item, placeholder_data):
"""Resave changed properties for placeholders"""
item_id, metadata_item = self._get_item(placeholder_item)
stub = get_stub()
if not item_id:
stub.print_msg("Cannot find item for "
f"{placeholder_item.scene_identifier}")
return
metadata_item["data"] = placeholder_data
stub.imprint(item_id, metadata_item)
def _get_item(self, placeholder_item):
"""Returns item id and item metadata for placeholder from file meta"""
stub = get_stub()
placeholder_uuid = placeholder_item.scene_identifier
for metadata_item in stub.get_metadata():
if not metadata_item.get("is_placeholder"):
continue
if placeholder_uuid in metadata_item.get("uuid"):
return metadata_item["members"][0], metadata_item
return None, None
def _collect_scene_placeholders(self):
"""" Cache placeholder data to shared data.
Returns:
(list) of dicts
"""
placeholder_items = self.builder.get_shared_populate_data(
"placeholder_items"
)
if not placeholder_items:
placeholder_items = []
for item in get_stub().get_metadata():
if not item.get("is_placeholder"):
continue
placeholder_items.append(item)
self.builder.set_shared_populate_data(
"placeholder_items", placeholder_items
)
return placeholder_items
def _imprint_item(self, item_id, name, placeholder_data, stub):
if not item_id:
raise ValueError("Couldn't create a placeholder")
container_data = {
"id": "openpype.placeholder",
"name": name,
"is_placeholder": True,
"plugin_identifier": self.identifier,
"uuid": str(uuid.uuid4()), # scene_identifier
"data": placeholder_data,
"members": [item_id]
}
stub.imprint(item_id, container_data)
class AEPlaceholderCreatePlugin(AEPlaceholderPlugin, PlaceholderCreateMixin):
"""Adds Create placeholder.
This adds composition and runs Create
"""
identifier = "aftereffects.create"
label = "AfterEffects create"
def create_placeholder(self, placeholder_data):
stub = get_stub()
name = "CREATEPLACEHOLDER"
item_id = stub.add_item(name, "COMP")
self._imprint_item(item_id, name, placeholder_data, stub)
def populate_placeholder(self, placeholder):
"""Replace 'placeholder' with publishable instance.
Renames prepared composition name, creates publishable instance, sets
frame/duration settings according to DB.
"""
pre_create_data = {"use_selection": True}
item_id, item = self._get_item(placeholder)
get_stub().select_items([item_id])
self.populate_create_placeholder(placeholder, pre_create_data)
# apply settings for populated composition
item_id, metadata_item = self._get_item(placeholder)
set_settings(True, True, [item_id])
def get_placeholder_options(self, options=None):
return self.get_create_plugin_options(options)
class AEPlaceholderLoadPlugin(AEPlaceholderPlugin, PlaceholderLoadMixin):
identifier = "aftereffects.load"
label = "AfterEffects load"
def create_placeholder(self, placeholder_data):
"""Creates AE's Placeholder item in Project items list.
Sets dummy resolution/duration/fps settings, will be replaced when
populated.
"""
stub = get_stub()
name = "LOADERPLACEHOLDER"
item_id = stub.add_placeholder(name, 1920, 1060, 25, 10)
self._imprint_item(item_id, name, placeholder_data, stub)
def populate_placeholder(self, placeholder):
"""Use Openpype Loader from `placeholder` to create new FootageItems
New FootageItems are created, files are imported.
"""
self.populate_load_placeholder(placeholder)
errors = placeholder.get_errors()
stub = get_stub()
if errors:
stub.print_msg("\n".join(errors))
else:
if not placeholder.data["keep_placeholder"]:
metadata = stub.get_metadata()
for item in metadata:
if not item.get("is_placeholder"):
continue
scene_identifier = item.get("uuid")
if (scene_identifier and
scene_identifier == placeholder.scene_identifier):
stub.delete_item(item["members"][0])
stub.remove_instance(placeholder.scene_identifier, metadata)
def get_placeholder_options(self, options=None):
return self.get_load_plugin_options(options)
def load_succeed(self, placeholder, container):
placeholder_item_id, _ = self._get_item(placeholder)
item_id = container.id
get_stub().add_item_instead_placeholder(placeholder_item_id, item_id)
def build_workfile_template(*args, **kwargs):
builder = AETemplateBuilder(registered_host())
builder.build_template(*args, **kwargs)
def update_workfile_template(*args):
builder = AETemplateBuilder(registered_host())
builder.rebuild_template()
def create_placeholder(*args):
"""Called when new workile placeholder should be created."""
host = registered_host()
builder = AETemplateBuilder(host)
window = WorkfileBuildPlaceholderDialog(host, builder)
window.exec_()
def update_placeholder(*args):
"""Called after placeholder item is selected to modify it."""
host = registered_host()
builder = AETemplateBuilder(host)
stub = get_stub()
selected_items = stub.get_selected_items(True, True, True)
if len(selected_items) != 1:
stub.print_msg("Please select just 1 placeholder")
return
selected_id = selected_items[0].id
placeholder_item = None
placeholder_items_by_id = {
placeholder_item.scene_identifier: placeholder_item
for placeholder_item in builder.get_placeholders()
}
for metadata_item in stub.get_metadata():
if not metadata_item.get("is_placeholder"):
continue
if selected_id in metadata_item.get("members"):
placeholder_item = placeholder_items_by_id.get(
metadata_item["uuid"])
break
if not placeholder_item:
stub.print_msg("Didn't find placeholder metadata. "
"Remove and re-create placeholder.")
return
window = WorkfileBuildPlaceholderDialog(host, builder)
window.set_update_mode(placeholder_item)
window.exec_()
================================================
FILE: openpype/hosts/aftereffects/api/ws_stub.py
================================================
"""
Stub handling connection from server to client.
Used anywhere solution is calling client methods.
"""
import json
import logging
import attr
from wsrpc_aiohttp import WebSocketAsync
from openpype.tools.adobe_webserver.app import WebServerTool
class ConnectionNotEstablishedYet(Exception):
pass
@attr.s
class AEItem(object):
"""
Object denoting Item in AE. Each item is created in AE by any Loader,
but contains same fields, which are being used in later processing.
"""
# metadata
id = attr.ib() # id created by AE, could be used for querying
name = attr.ib() # name of item
item_type = attr.ib(default=None) # item type (footage, folder, comp)
# all imported elements, single for
# regular image, array for Backgrounds
members = attr.ib(factory=list)
frameStart = attr.ib(default=None)
framesDuration = attr.ib(default=None)
frameRate = attr.ib(default=None)
file_name = attr.ib(default=None)
instance_id = attr.ib(default=None) # New Publisher
width = attr.ib(default=None)
height = attr.ib(default=None)
is_placeholder = attr.ib(default=False)
uuid = attr.ib(default=False)
path = attr.ib(default=False) # path to FootageItem to validate
# list of composition Footage is in
containing_comps = attr.ib(factory=list)
class AfterEffectsServerStub():
"""
Stub for calling function on client (Photoshop js) side.
Expects that client is already connected (started when avalon menu
is opened).
'self.websocketserver.call' is used as async wrapper
"""
PUBLISH_ICON = '\u2117 '
LOADED_ICON = '\u25bc'
def __init__(self):
self.websocketserver = WebServerTool.get_instance()
self.client = self.get_client()
self.log = logging.getLogger(self.__class__.__name__)
@staticmethod
def get_client():
"""
Return first connected client to WebSocket
TODO implement selection by Route
:return:
payload(dict): - dictionary from json representation, expected to
come from _handle_return
"""
if not payload:
return []
if isinstance(payload, str): # safety fallback
try:
payload = json.loads(payload)
except json.decoder.JSONDecodeError:
raise ValueError("Received broken JSON {}".format(payload))
if isinstance(payload, dict):
payload = [payload]
ret = []
# convert to AEItem to use dot donation
for d in payload:
if not d:
continue
# currently implemented and expected fields
item = AEItem(d.get('id'),
d.get('name'),
d.get('type'),
d.get('members'),
d.get('frameStart'),
d.get('framesDuration'),
d.get('frameRate'),
d.get('file_name'),
d.get("instance_id"),
d.get("width"),
d.get("height"),
d.get("is_placeholder"),
d.get("uuid"),
d.get("path"),
d.get("containing_comps"),)
ret.append(item)
return ret
def get_stub():
"""
Convenience function to get server RPC stub to call methods directed
for host (Photoshop).
It expects already created connection, started from client.
Currently created when panel is opened (PS: Window>Extensions>Avalon)
:return:
"
invalid_setting_str = "Found invalid settings:
{}".\
format(break_str.join(invalid_settings))
formatting_data = {
"invalid_setting_str": invalid_setting_str,
"invalid_keys_str": invalid_keys_str
}
raise PublishXmlValidationError(self, msg,
formatting_data=formatting_data)
if not os.path.exists(instance.data.get("source")):
scene_url = instance.data.get("source")
msg = "Scene file {} not found (saved under wrong name)".format(
scene_url
)
formatting_data = {
"scene_url": scene_url
}
raise PublishXmlValidationError(self, msg, key="file_not_found",
formatting_data=formatting_data)
================================================
FILE: openpype/hosts/blender/__init__.py
================================================
from .addon import BlenderAddon
__all__ = (
"BlenderAddon",
)
================================================
FILE: openpype/hosts/blender/addon.py
================================================
import os
from openpype.modules import OpenPypeModule, IHostAddon
BLENDER_ROOT_DIR = os.path.dirname(os.path.abspath(__file__))
class BlenderAddon(OpenPypeModule, IHostAddon):
name = "blender"
host_name = "blender"
def initialize(self, module_settings):
self.enabled = True
def add_implementation_envs(self, env, _app):
"""Modify environments to contain all required for implementation."""
# Prepare path to implementation script
implementation_user_script_path = os.path.join(
BLENDER_ROOT_DIR,
"blender_addon"
)
# Add blender implementation script path to PYTHONPATH
python_path = env.get("PYTHONPATH") or ""
python_path_parts = [
path
for path in python_path.split(os.pathsep)
if path
]
python_path_parts.insert(0, implementation_user_script_path)
env["PYTHONPATH"] = os.pathsep.join(python_path_parts)
# Modify Blender user scripts path
previous_user_scripts = set()
# Implementation path is added to set for easier paths check inside
# loops - will be removed at the end
previous_user_scripts.add(implementation_user_script_path)
openpype_blender_user_scripts = (
env.get("OPENPYPE_BLENDER_USER_SCRIPTS") or ""
)
for path in openpype_blender_user_scripts.split(os.pathsep):
if path:
previous_user_scripts.add(os.path.normpath(path))
blender_user_scripts = env.get("BLENDER_USER_SCRIPTS") or ""
for path in blender_user_scripts.split(os.pathsep):
if path:
previous_user_scripts.add(os.path.normpath(path))
# Remove implementation path from user script paths as is set to
# `BLENDER_USER_SCRIPTS`
previous_user_scripts.remove(implementation_user_script_path)
env["BLENDER_USER_SCRIPTS"] = implementation_user_script_path
# Set custom user scripts env
env["OPENPYPE_BLENDER_USER_SCRIPTS"] = os.pathsep.join(
previous_user_scripts
)
# Define Qt binding if not defined
if not env.get("QT_PREFERRED_BINDING"):
env["QT_PREFERRED_BINDING"] = "PySide2"
def get_launch_hook_paths(self, app):
if app.host_name != self.host_name:
return []
return [
os.path.join(BLENDER_ROOT_DIR, "hooks")
]
def get_workfile_extensions(self):
return [".blend"]
================================================
FILE: openpype/hosts/blender/api/__init__.py
================================================
"""Public API
Anything that isn't defined here is INTERNAL and unreliable for external use.
"""
from .pipeline import (
install,
uninstall,
ls,
publish,
containerise,
BlenderHost,
)
from .plugin import (
Creator,
Loader,
)
from .workio import (
open_file,
save_file,
current_file,
has_unsaved_changes,
file_extensions,
work_root,
)
from .lib import (
lsattr,
lsattrs,
read,
maintained_selection,
maintained_time,
get_selection,
# unique_name,
)
from .capture import capture
from .render_lib import prepare_rendering
__all__ = [
"install",
"uninstall",
"ls",
"publish",
"containerise",
"BlenderHost",
"Creator",
"Loader",
# Workfiles API
"open_file",
"save_file",
"current_file",
"has_unsaved_changes",
"file_extensions",
"work_root",
# Utility functions
"maintained_selection",
"maintained_time",
"lsattr",
"lsattrs",
"read",
"get_selection",
"capture",
# "unique_name",
"prepare_rendering",
]
================================================
FILE: openpype/hosts/blender/api/action.py
================================================
import bpy
import pyblish.api
from openpype.pipeline.publish import get_errored_instances_from_context
class SelectInvalidAction(pyblish.api.Action):
"""Select invalid objects in Blender when a publish plug-in failed."""
label = "Select Invalid"
on = "failed"
icon = "search"
def process(self, context, plugin):
errored_instances = get_errored_instances_from_context(context,
plugin=plugin)
# Get the invalid nodes for the plug-ins
self.log.info("Finding invalid nodes...")
invalid = list()
for instance in errored_instances:
invalid_nodes = plugin.get_invalid(instance)
if invalid_nodes:
if isinstance(invalid_nodes, (list, tuple)):
invalid.extend(invalid_nodes)
else:
self.log.warning(
"Failed plug-in doesn't have any selectable objects."
)
bpy.ops.object.select_all(action='DESELECT')
# Make sure every node is only processed once
invalid = list(set(invalid))
if not invalid:
self.log.info("No invalid nodes found.")
return
invalid_names = [obj.name for obj in invalid]
self.log.info(
"Selecting invalid objects: %s", ", ".join(invalid_names)
)
# Select the objects and also make the last one the active object.
for obj in invalid:
obj.select_set(True)
bpy.context.view_layer.objects.active = invalid[-1]
================================================
FILE: openpype/hosts/blender/api/capture.py
================================================
"""Blender Capture
Playblasting with independent viewport, camera and display options
"""
import contextlib
import bpy
from .lib import maintained_time
from .plugin import deselect_all, create_blender_context
def capture(
camera=None,
width=None,
height=None,
filename=None,
start_frame=None,
end_frame=None,
step_frame=None,
sound=None,
isolate=None,
maintain_aspect_ratio=True,
overwrite=False,
image_settings=None,
display_options=None
):
"""Playblast in an independent windows
Arguments:
camera (str, optional): Name of camera, defaults to "Camera"
width (int, optional): Width of output in pixels
height (int, optional): Height of output in pixels
filename (str, optional): Name of output file path. Defaults to current
render output path.
start_frame (int, optional): Defaults to current start frame.
end_frame (int, optional): Defaults to current end frame.
step_frame (int, optional): Defaults to 1.
sound (str, optional): Specify the sound node to be used during
playblast. When None (default) no sound will be used.
isolate (list): List of nodes to isolate upon capturing
maintain_aspect_ratio (bool, optional): Modify height in order to
maintain aspect ratio.
overwrite (bool, optional): Whether or not to overwrite if file
already exists. If disabled and file exists and error will be
raised.
image_settings (dict, optional): Supplied image settings for render,
using `ImageSettings`
display_options (dict, optional): Supplied display options for render
"""
scene = bpy.context.scene
camera = camera or "Camera"
# Ensure camera exists.
if camera not in scene.objects and camera != "AUTO":
raise RuntimeError("Camera does not exist: {0}".format(camera))
# Ensure resolution.
if width and height:
maintain_aspect_ratio = False
width = width or scene.render.resolution_x
height = height or scene.render.resolution_y
if maintain_aspect_ratio:
ratio = scene.render.resolution_x / scene.render.resolution_y
height = round(width / ratio)
# Get frame range.
if start_frame is None:
start_frame = scene.frame_start
if end_frame is None:
end_frame = scene.frame_end
if step_frame is None:
step_frame = 1
frame_range = (start_frame, end_frame, step_frame)
if filename is None:
filename = scene.render.filepath
render_options = {
"filepath": "{}.".format(filename.rstrip(".")),
"resolution_x": width,
"resolution_y": height,
"use_overwrite": overwrite,
}
with _independent_window() as window:
applied_view(window, camera, isolate, options=display_options)
with contextlib.ExitStack() as stack:
stack.enter_context(maintain_camera(window, camera))
stack.enter_context(applied_frame_range(window, *frame_range))
stack.enter_context(applied_render_options(window, render_options))
stack.enter_context(applied_image_settings(window, image_settings))
stack.enter_context(maintained_time())
bpy.ops.render.opengl(
animation=True,
render_keyed_only=False,
sequencer=False,
write_still=False,
view_context=True
)
return filename
ImageSettings = {
"file_format": "FFMPEG",
"color_mode": "RGB",
"ffmpeg": {
"format": "QUICKTIME",
"use_autosplit": False,
"codec": "H264",
"constant_rate_factor": "MEDIUM",
"gopsize": 18,
"use_max_b_frames": False,
},
}
def isolate_objects(window, objects):
"""Isolate selection"""
deselect_all()
for obj in objects:
obj.select_set(True)
context = create_blender_context(selected=objects, window=window)
with bpy.context.temp_override(**context):
bpy.ops.view3d.view_axis(type="FRONT")
bpy.ops.view3d.localview()
deselect_all()
def _apply_options(entity, options):
for option, value in options.items():
if isinstance(value, dict):
_apply_options(getattr(entity, option), value)
else:
setattr(entity, option, value)
def applied_view(window, camera, isolate=None, options=None):
"""Apply view options to window."""
area = window.screen.areas[0]
space = area.spaces[0]
area.ui_type = "VIEW_3D"
types = {"MESH", "GPENCIL"}
objects = [obj for obj in window.scene.objects if obj.type in types]
if camera == "AUTO":
space.region_3d.view_perspective = "ORTHO"
isolate_objects(window, isolate or objects)
else:
isolate_objects(window, isolate or objects)
space.camera = window.scene.objects.get(camera)
space.region_3d.view_perspective = "CAMERA"
if isinstance(options, dict):
_apply_options(space, options)
else:
space.shading.type = "SOLID"
space.shading.color_type = "MATERIAL"
space.show_gizmo = False
space.overlay.show_overlays = False
@contextlib.contextmanager
def applied_frame_range(window, start, end, step):
"""Context manager for setting frame range."""
# Store current frame range
current_frame_start = window.scene.frame_start
current_frame_end = window.scene.frame_end
current_frame_step = window.scene.frame_step
# Apply frame range
window.scene.frame_start = start
window.scene.frame_end = end
window.scene.frame_step = step
try:
yield
finally:
# Restore frame range
window.scene.frame_start = current_frame_start
window.scene.frame_end = current_frame_end
window.scene.frame_step = current_frame_step
@contextlib.contextmanager
def applied_render_options(window, options):
"""Context manager for setting render options."""
render = window.scene.render
# Store current settings
original = {}
for opt in options.copy():
try:
original[opt] = getattr(render, opt)
except ValueError:
options.pop(opt)
# Apply settings
_apply_options(render, options)
try:
yield
finally:
# Restore previous settings
_apply_options(render, original)
@contextlib.contextmanager
def applied_image_settings(window, options):
"""Context manager to override image settings."""
options = options or ImageSettings.copy()
ffmpeg = options.pop("ffmpeg", {})
render = window.scene.render
# Store current image settings
original = {}
for opt in options.copy():
try:
original[opt] = getattr(render.image_settings, opt)
except ValueError:
options.pop(opt)
# Store current ffmpeg settings
original_ffmpeg = {}
for opt in ffmpeg.copy():
try:
original_ffmpeg[opt] = getattr(render.ffmpeg, opt)
except ValueError:
ffmpeg.pop(opt)
# Apply image settings
for opt, value in options.items():
setattr(render.image_settings, opt, value)
# Apply ffmpeg settings
for opt, value in ffmpeg.items():
setattr(render.ffmpeg, opt, value)
try:
yield
finally:
# Restore previous settings
for opt, value in original.items():
setattr(render.image_settings, opt, value)
for opt, value in original_ffmpeg.items():
setattr(render.ffmpeg, opt, value)
@contextlib.contextmanager
def maintain_camera(window, camera):
"""Context manager to override camera."""
current_camera = window.scene.camera
if camera in window.scene.objects:
window.scene.camera = window.scene.objects.get(camera)
try:
yield
finally:
window.scene.camera = current_camera
@contextlib.contextmanager
def _independent_window():
"""Create capture-window context."""
context = create_blender_context()
current_windows = set(bpy.context.window_manager.windows)
with bpy.context.temp_override(**context):
bpy.ops.wm.window_new()
window = list(
set(bpy.context.window_manager.windows) - current_windows)[0]
context["window"] = window
try:
yield window
finally:
bpy.ops.wm.window_close()
================================================
FILE: openpype/hosts/blender/api/colorspace.py
================================================
import attr
import bpy
@attr.s
class LayerMetadata(object):
"""Data class for Render Layer metadata."""
frameStart = attr.ib()
frameEnd = attr.ib()
@attr.s
class RenderProduct(object):
"""
Getting Colorspace as Specific Render Product Parameter for submitting
publish job.
"""
colorspace = attr.ib() # colorspace
view = attr.ib() # OCIO view transform
productName = attr.ib(default=None)
class ARenderProduct(object):
def __init__(self):
"""Constructor."""
# Initialize
self.layer_data = self._get_layer_data()
self.layer_data.products = self.get_render_products()
def _get_layer_data(self):
scene = bpy.context.scene
return LayerMetadata(
frameStart=int(scene.frame_start),
frameEnd=int(scene.frame_end),
)
def get_render_products(self):
"""To be implemented by renderer class.
This should return a list of RenderProducts.
Returns:
list: List of RenderProduct
"""
return [
RenderProduct(
colorspace="sRGB",
view="ACES 1.0",
productName=""
)
]
================================================
FILE: openpype/hosts/blender/api/lib.py
================================================
import os
import traceback
import importlib
import contextlib
from typing import Dict, List, Union
import bpy
import addon_utils
from openpype.lib import Logger
from . import pipeline
log = Logger.get_logger(__name__)
def load_scripts(paths):
"""Copy of `load_scripts` from Blender's implementation.
It is possible that this function will be changed in future and usage will
be based on Blender version.
"""
import bpy_types
loaded_modules = set()
previous_classes = [
cls
for cls in bpy.types.bpy_struct.__subclasses__()
]
def register_module_call(mod):
register = getattr(mod, "register", None)
if register:
try:
register()
except:
traceback.print_exc()
else:
print("\nWarning! '%s' has no register function, "
"this is now a requirement for registerable scripts" %
mod.__file__)
def unregister_module_call(mod):
unregister = getattr(mod, "unregister", None)
if unregister:
try:
unregister()
except:
traceback.print_exc()
def test_reload(mod):
# reloading this causes internal errors
# because the classes from this module are stored internally
# possibly to refresh internal references too but for now, best not to.
if mod == bpy_types:
return mod
try:
return importlib.reload(mod)
except:
traceback.print_exc()
def test_register(mod):
if mod:
register_module_call(mod)
bpy.utils._global_loaded_modules.append(mod.__name__)
from bpy_restrict_state import RestrictBlend
with RestrictBlend():
for base_path in paths:
for path_subdir in bpy.utils._script_module_dirs:
path = os.path.join(base_path, path_subdir)
if not os.path.isdir(path):
continue
bpy.utils._sys_path_ensure_prepend(path)
# Only add to 'sys.modules' unless this is 'startup'.
if path_subdir != "startup":
continue
for mod in bpy.utils.modules_from_path(path, loaded_modules):
test_register(mod)
addons_paths = []
for base_path in paths:
addons_path = os.path.join(base_path, "addons")
if not os.path.exists(addons_path):
continue
addons_paths.append(addons_path)
addons_module_path = os.path.join(addons_path, "modules")
if os.path.exists(addons_module_path):
bpy.utils._sys_path_ensure_prepend(addons_module_path)
if addons_paths:
# Fake addons
origin_paths = addon_utils.paths
def new_paths():
paths = origin_paths() + addons_paths
return paths
addon_utils.paths = new_paths
addon_utils.modules_refresh()
# load template (if set)
if any(bpy.utils.app_template_paths()):
import bl_app_template_utils
bl_app_template_utils.reset(reload_scripts=False)
del bl_app_template_utils
for cls in bpy.types.bpy_struct.__subclasses__():
if cls in previous_classes:
continue
if not getattr(cls, "is_registered", False):
continue
for subcls in cls.__subclasses__():
if not subcls.is_registered:
print(
"Warning, unregistered class: %s(%s)" %
(subcls.__name__, cls.__name__)
)
def append_user_scripts():
user_scripts = os.environ.get("OPENPYPE_BLENDER_USER_SCRIPTS")
if not user_scripts:
return
try:
load_scripts(user_scripts.split(os.pathsep))
except Exception:
print("Couldn't load user scripts \"{}\"".format(user_scripts))
traceback.print_exc()
def set_app_templates_path():
# Blender requires the app templates to be in `BLENDER_USER_SCRIPTS`.
# After running Blender, we set that variable to our custom path, so
# that the user can use their custom app templates.
# We look among the scripts paths for one of the paths that contains
# the app templates. The path must contain the subfolder
# `startup/bl_app_templates_user`.
paths = os.environ.get("OPENPYPE_BLENDER_USER_SCRIPTS").split(os.pathsep)
app_templates_path = None
for path in paths:
if os.path.isdir(
os.path.join(path, "startup", "bl_app_templates_user")):
app_templates_path = path
break
if app_templates_path and os.path.isdir(app_templates_path):
os.environ["BLENDER_USER_SCRIPTS"] = app_templates_path
def imprint(node: bpy.types.bpy_struct_meta_idprop, data: Dict):
r"""Write `data` to `node` as userDefined attributes
Arguments:
node: Long name of node
data: Dictionary of key/value pairs
Example:
>>> import bpy
>>> def compute():
... return 6
...
>>> bpy.ops.mesh.primitive_cube_add()
>>> cube = bpy.context.view_layer.objects.active
>>> imprint(cube, {
... "regularString": "myFamily",
... "computedValue": lambda: compute()
... })
...
>>> cube['avalon']['computedValue']
6
"""
imprint_data = dict()
for key, value in data.items():
if value is None:
continue
if callable(value):
# Support values evaluated at imprint
value = value()
if not isinstance(value, (int, float, bool, str, list, dict)):
raise TypeError(f"Unsupported type: {type(value)}")
imprint_data[key] = value
pipeline.metadata_update(node, imprint_data)
def lsattr(attr: str,
value: Union[str, int, bool, List, Dict, None] = None) -> List:
r"""Return nodes matching `attr` and `value`
Arguments:
attr: Name of Blender property
value: Value of attribute. If none
is provided, return all nodes with this attribute.
Example:
>>> lsattr("id", "myId")
... [bpy.data.objects["myNode"]
>>> lsattr("id")
... [bpy.data.objects["myNode"], bpy.data.objects["myOtherNode"]]
Returns:
list
"""
return lsattrs({attr: value})
def lsattrs(attrs: Dict) -> List:
r"""Return nodes with the given attribute(s).
Arguments:
attrs: Name and value pairs of expected matches
Example:
>>> lsattrs({"age": 5}) # Return nodes with an `age` of 5
# Return nodes with both `age` and `color` of 5 and blue
>>> lsattrs({"age": 5, "color": "blue"})
Returns a list.
"""
# For now return all objects, not filtered by scene/collection/view_layer.
matches = set()
for coll in dir(bpy.data):
if not isinstance(
getattr(bpy.data, coll),
bpy.types.bpy_prop_collection,
):
continue
for node in getattr(bpy.data, coll):
for attr, value in attrs.items():
avalon_prop = node.get(pipeline.AVALON_PROPERTY)
if not avalon_prop:
continue
if (avalon_prop.get(attr)
and (value is None or avalon_prop.get(attr) == value)):
matches.add(node)
return list(matches)
def read(node: bpy.types.bpy_struct_meta_idprop):
"""Return user-defined attributes from `node`"""
data = dict(node.get(pipeline.AVALON_PROPERTY, {}))
# Ignore hidden/internal data
data = {
key: value
for key, value in data.items() if not key.startswith("_")
}
return data
def get_selected_collections():
"""
Returns a list of the currently selected collections in the outliner.
Raises:
RuntimeError: If the outliner cannot be found in the main Blender
window.
Returns:
list: A list of `bpy.types.Collection` objects that are currently
selected in the outliner.
"""
window = bpy.context.window or bpy.context.window_manager.windows[0]
try:
area = next(
area for area in window.screen.areas
if area.type == 'OUTLINER')
region = next(
region for region in area.regions
if region.type == 'WINDOW')
except StopIteration as e:
raise RuntimeError("Could not find outliner. An outliner space "
"must be in the main Blender window.") from e
with bpy.context.temp_override(
window=window,
area=area,
region=region,
screen=window.screen
):
ids = bpy.context.selected_ids
return [id for id in ids if isinstance(id, bpy.types.Collection)]
def get_selection(include_collections: bool = False) -> List[bpy.types.Object]:
"""
Returns a list of selected objects in the current Blender scene.
Args:
include_collections (bool, optional): Whether to include selected
collections in the result. Defaults to False.
Returns:
List[bpy.types.Object]: A list of selected objects.
"""
selection = [obj for obj in bpy.context.scene.objects if obj.select_get()]
if include_collections:
selection.extend(get_selected_collections())
return selection
@contextlib.contextmanager
def maintained_selection():
r"""Maintain selection during context
Example:
>>> with maintained_selection():
... # Modify selection
... bpy.ops.object.select_all(action='DESELECT')
>>> # Selection restored
"""
previous_selection = get_selection()
previous_active = bpy.context.view_layer.objects.active
try:
yield
finally:
# Clear the selection
for node in get_selection():
node.select_set(state=False)
if previous_selection:
for node in previous_selection:
try:
node.select_set(state=True)
except ReferenceError:
# This could happen if a selected node was deleted during
# the context.
log.exception("Failed to reselect")
continue
try:
bpy.context.view_layer.objects.active = previous_active
except ReferenceError:
# This could happen if the active node was deleted during the
# context.
log.exception("Failed to set active object.")
@contextlib.contextmanager
def maintained_time():
"""Maintain current frame during context."""
current_time = bpy.context.scene.frame_current
try:
yield
finally:
bpy.context.scene.frame_current = current_time
================================================
FILE: openpype/hosts/blender/api/ops.py
================================================
"""Blender operators and menus for use with Avalon."""
import os
import sys
import platform
import time
import traceback
import collections
from pathlib import Path
from types import ModuleType
from typing import Dict, List, Optional, Union
from qtpy import QtWidgets, QtCore
import bpy
import bpy.utils.previews
from openpype import style
from openpype import AYON_SERVER_ENABLED
from openpype.pipeline import get_current_asset_name, get_current_task_name
from openpype.tools.utils import host_tools
from .workio import OpenFileCacher
from . import pipeline
PREVIEW_COLLECTIONS: Dict = dict()
# This seems like a good value to keep the Qt app responsive and doesn't slow
# down Blender. At least on macOS I the interface of Blender gets very laggy if
# you make it smaller.
TIMER_INTERVAL: float = 0.01 if platform.system() == "Windows" else 0.1
def execute_function_in_main_thread(f):
"""Decorator to move a function call into main thread items"""
def wrapper(*args, **kwargs):
mti = MainThreadItem(f, *args, **kwargs)
execute_in_main_thread(mti)
return wrapper
class BlenderApplication(QtWidgets.QApplication):
_instance = None
blender_windows = {}
def __init__(self, *args, **kwargs):
super(BlenderApplication, self).__init__(*args, **kwargs)
self.setQuitOnLastWindowClosed(False)
self.setStyleSheet(style.load_stylesheet())
self.lastWindowClosed.connect(self.__class__.reset)
@classmethod
def get_app(cls):
if cls._instance is None:
cls._instance = cls(sys.argv)
return cls._instance
@classmethod
def reset(cls):
cls._instance = None
@classmethod
def store_window(cls, identifier, window):
current_window = cls.get_window(identifier)
cls.blender_windows[identifier] = window
if current_window:
current_window.close()
# current_window.deleteLater()
@classmethod
def get_window(cls, identifier):
return cls.blender_windows.get(identifier)
class MainThreadItem:
"""Structure to store information about callback in main thread.
Item should be used to execute callback in main thread which may be needed
for execution of Qt objects.
Item store callback (callable variable), arguments and keyword arguments
for the callback. Item hold information about it's process.
"""
not_set = object()
sleep_time = 0.1
def __init__(self, callback, *args, **kwargs):
self.done = False
self.exception = self.not_set
self.result = self.not_set
self.callback = callback
self.args = args
self.kwargs = kwargs
def execute(self):
"""Execute callback and store its result.
Method must be called from main thread. Item is marked as `done`
when callback execution finished. Store output of callback of exception
information when callback raises one.
"""
print("Executing process in main thread")
if self.done:
print("- item is already processed")
return
callback = self.callback
args = self.args
kwargs = self.kwargs
print("Running callback: {}".format(str(callback)))
try:
result = callback(*args, **kwargs)
self.result = result
except Exception:
self.exception = sys.exc_info()
finally:
print("Done")
self.done = True
def wait(self):
"""Wait for result from main thread.
This method stops current thread until callback is executed.
Returns:
object: Output of callback. May be any type or object.
Raises:
Exception: Reraise any exception that happened during callback
execution.
"""
while not self.done:
print(self.done)
time.sleep(self.sleep_time)
if self.exception is self.not_set:
return self.result
raise self.exception
class GlobalClass:
app = None
main_thread_callbacks = collections.deque()
is_windows = platform.system().lower() == "windows"
def execute_in_main_thread(main_thead_item):
print("execute_in_main_thread")
GlobalClass.main_thread_callbacks.append(main_thead_item)
def _process_app_events() -> Optional[float]:
"""Process the events of the Qt app if the window is still visible.
If the app has any top level windows and at least one of them is visible
return the time after which this function should be run again. Else return
None, so the function is not run again and will be unregistered.
"""
while GlobalClass.main_thread_callbacks:
main_thread_item = GlobalClass.main_thread_callbacks.popleft()
main_thread_item.execute()
if main_thread_item.exception is not MainThreadItem.not_set:
_clc, val, tb = main_thread_item.exception
msg = str(val)
detail = "\n".join(traceback.format_exception(_clc, val, tb))
dialog = QtWidgets.QMessageBox(
QtWidgets.QMessageBox.Warning,
"Error",
msg)
dialog.setMinimumWidth(500)
dialog.setDetailedText(detail)
dialog.exec_()
# Refresh Manager
if GlobalClass.app:
manager = GlobalClass.app.get_window("WM_OT_avalon_manager")
if manager:
manager.refresh()
if not GlobalClass.is_windows:
if OpenFileCacher.opening_file:
return TIMER_INTERVAL
app = GlobalClass.app
if app._instance:
app.processEvents()
return TIMER_INTERVAL
return TIMER_INTERVAL
class LaunchQtApp(bpy.types.Operator):
"""A Base class for opertors to launch a Qt app."""
_app: QtWidgets.QApplication
_window = Union[QtWidgets.QDialog, ModuleType]
_tool_name: str = None
_init_args: Optional[List] = list()
_init_kwargs: Optional[Dict] = dict()
bl_idname: str = None
def __init__(self):
if self.bl_idname is None:
raise NotImplementedError("Attribute `bl_idname` must be set!")
print(f"Initialising {self.bl_idname}...")
self._app = BlenderApplication.get_app()
GlobalClass.app = self._app
if not bpy.app.timers.is_registered(_process_app_events):
bpy.app.timers.register(
_process_app_events,
persistent=True
)
def execute(self, context):
"""Execute the operator.
The child class must implement `execute()` where it only has to set
`self._window` to the desired Qt window and then simply run
`return super().execute(context)`.
`self._window` is expected to have a `show` method.
If the `show` method requires arguments, you can set `self._show_args`
and `self._show_kwargs`. `args` should be a list, `kwargs` a
dictionary.
"""
if self._tool_name is None:
if self._window is None:
raise AttributeError("`self._window` is not set.")
else:
window = self._app.get_window(self.bl_idname)
if window is None:
window = host_tools.get_tool_by_name(self._tool_name)
self._app.store_window(self.bl_idname, window)
self._window = window
if not isinstance(self._window, (QtWidgets.QWidget, ModuleType)):
raise AttributeError(
"`window` should be a `QWidget or module`. Got: {}".format(
str(type(window))
)
)
self.before_window_show()
def pull_to_front(window):
"""Pull window forward to screen.
If Window is minimized this will un-minimize, then it can be raised
and activated to the front.
"""
window.setWindowState(
(window.windowState() & ~QtCore.Qt.WindowMinimized) |
QtCore.Qt.WindowActive
)
window.raise_()
window.activateWindow()
if isinstance(self._window, ModuleType):
self._window.show()
pull_to_front(self._window)
# Pull window to the front
window = None
if hasattr(self._window, "window"):
window = self._window.window
elif hasattr(self._window, "_window"):
window = self._window.window
if window:
self._app.store_window(self.bl_idname, window)
else:
origin_flags = self._window.windowFlags()
on_top_flags = origin_flags | QtCore.Qt.WindowStaysOnTopHint
self._window.setWindowFlags(on_top_flags)
self._window.show()
pull_to_front(self._window)
# if on_top_flags != origin_flags:
# self._window.setWindowFlags(origin_flags)
# self._window.show()
return {'FINISHED'}
def before_window_show(self):
return
class LaunchCreator(LaunchQtApp):
"""Launch Avalon Creator."""
bl_idname = "wm.avalon_creator"
bl_label = "Create..."
_tool_name = "creator"
def before_window_show(self):
self._window.refresh()
def execute(self, context):
host_tools.show_publisher(tab="create")
return {"FINISHED"}
class LaunchLoader(LaunchQtApp):
"""Launch Avalon Loader."""
bl_idname = "wm.avalon_loader"
bl_label = "Load..."
_tool_name = "loader"
def before_window_show(self):
if AYON_SERVER_ENABLED:
return
self._window.set_context(
{"asset": get_current_asset_name()},
refresh=True
)
class LaunchPublisher(LaunchQtApp):
"""Launch Avalon Publisher."""
bl_idname = "wm.avalon_publisher"
bl_label = "Publish..."
def execute(self, context):
host_tools.show_publisher(tab="publish")
return {"FINISHED"}
class LaunchManager(LaunchQtApp):
"""Launch Avalon Manager."""
bl_idname = "wm.avalon_manager"
bl_label = "Manage..."
_tool_name = "sceneinventory"
def before_window_show(self):
if AYON_SERVER_ENABLED:
return
self._window.refresh()
class LaunchLibrary(LaunchQtApp):
"""Launch Library Loader."""
bl_idname = "wm.library_loader"
bl_label = "Library..."
_tool_name = "libraryloader"
def before_window_show(self):
if AYON_SERVER_ENABLED:
return
self._window.refresh()
class LaunchWorkFiles(LaunchQtApp):
"""Launch Avalon Work Files."""
bl_idname = "wm.avalon_workfiles"
bl_label = "Work Files..."
_tool_name = "workfiles"
def execute(self, context):
result = super().execute(context)
if not AYON_SERVER_ENABLED:
self._window.set_context({
"asset": get_current_asset_name(),
"task": get_current_task_name()
})
return result
def before_window_show(self):
if AYON_SERVER_ENABLED:
return
self._window.root = str(Path(
os.environ.get("AVALON_WORKDIR", ""),
os.environ.get("AVALON_SCENEDIR", ""),
))
self._window.refresh()
class SetFrameRange(bpy.types.Operator):
bl_idname = "wm.ayon_set_frame_range"
bl_label = "Set Frame Range"
def execute(self, context):
data = pipeline.get_asset_data()
pipeline.set_frame_range(data)
return {"FINISHED"}
class SetResolution(bpy.types.Operator):
bl_idname = "wm.ayon_set_resolution"
bl_label = "Set Resolution"
def execute(self, context):
data = pipeline.get_asset_data()
pipeline.set_resolution(data)
return {"FINISHED"}
class TOPBAR_MT_avalon(bpy.types.Menu):
"""Avalon menu."""
bl_idname = "TOPBAR_MT_avalon"
bl_label = os.environ.get("AVALON_LABEL")
def draw(self, context):
"""Draw the menu in the UI."""
layout = self.layout
pcoll = PREVIEW_COLLECTIONS.get("avalon")
if pcoll:
pyblish_menu_icon = pcoll["pyblish_menu_icon"]
pyblish_menu_icon_id = pyblish_menu_icon.icon_id
else:
pyblish_menu_icon_id = 0
asset = get_current_asset_name()
task = get_current_task_name()
context_label = f"{asset}, {task}"
context_label_item = layout.row()
context_label_item.operator(
LaunchWorkFiles.bl_idname, text=context_label
)
context_label_item.enabled = False
layout.separator()
layout.operator(LaunchCreator.bl_idname, text="Create...")
layout.operator(LaunchLoader.bl_idname, text="Load...")
layout.operator(
LaunchPublisher.bl_idname,
text="Publish...",
icon_value=pyblish_menu_icon_id,
)
layout.operator(LaunchManager.bl_idname, text="Manage...")
layout.operator(LaunchLibrary.bl_idname, text="Library...")
layout.separator()
layout.operator(SetFrameRange.bl_idname, text="Set Frame Range")
layout.operator(SetResolution.bl_idname, text="Set Resolution")
layout.separator()
layout.operator(LaunchWorkFiles.bl_idname, text="Work Files...")
def draw_avalon_menu(self, context):
"""Draw the Avalon menu in the top bar."""
self.layout.menu(TOPBAR_MT_avalon.bl_idname)
classes = [
LaunchCreator,
LaunchLoader,
LaunchPublisher,
LaunchManager,
LaunchLibrary,
LaunchWorkFiles,
SetFrameRange,
SetResolution,
TOPBAR_MT_avalon,
]
def register():
"Register the operators and menu."
pcoll = bpy.utils.previews.new()
pyblish_icon_file = Path(__file__).parent / "icons" / "pyblish-32x32.png"
pcoll.load("pyblish_menu_icon", str(pyblish_icon_file.absolute()), 'IMAGE')
PREVIEW_COLLECTIONS["avalon"] = pcoll
BlenderApplication.get_app()
for cls in classes:
bpy.utils.register_class(cls)
bpy.types.TOPBAR_MT_editor_menus.append(draw_avalon_menu)
def unregister():
"""Unregister the operators and menu."""
pcoll = PREVIEW_COLLECTIONS.pop("avalon")
bpy.utils.previews.remove(pcoll)
bpy.types.TOPBAR_MT_editor_menus.remove(draw_avalon_menu)
for cls in reversed(classes):
bpy.utils.unregister_class(cls)
================================================
FILE: openpype/hosts/blender/api/pipeline.py
================================================
import os
import sys
import traceback
from typing import Callable, Dict, Iterator, List, Optional
import bpy
from . import lib
from . import ops
import pyblish.api
from openpype.host import (
HostBase,
IWorkfileHost,
IPublishHost,
ILoadHost
)
from openpype.client import get_asset_by_name
from openpype.pipeline import (
schema,
legacy_io,
get_current_project_name,
get_current_asset_name,
register_loader_plugin_path,
register_creator_plugin_path,
deregister_loader_plugin_path,
deregister_creator_plugin_path,
AVALON_CONTAINER_ID,
)
from openpype.lib import (
Logger,
register_event_callback,
emit_event
)
import openpype.hosts.blender
from openpype.settings import get_project_settings
from .workio import (
open_file,
save_file,
current_file,
has_unsaved_changes,
file_extensions,
work_root,
)
HOST_DIR = os.path.dirname(os.path.abspath(openpype.hosts.blender.__file__))
PLUGINS_DIR = os.path.join(HOST_DIR, "plugins")
PUBLISH_PATH = os.path.join(PLUGINS_DIR, "publish")
LOAD_PATH = os.path.join(PLUGINS_DIR, "load")
CREATE_PATH = os.path.join(PLUGINS_DIR, "create")
ORIGINAL_EXCEPTHOOK = sys.excepthook
AVALON_INSTANCES = "AVALON_INSTANCES"
AVALON_CONTAINERS = "AVALON_CONTAINERS"
AVALON_PROPERTY = 'avalon'
IS_HEADLESS = bpy.app.background
log = Logger.get_logger(__name__)
class BlenderHost(HostBase, IWorkfileHost, IPublishHost, ILoadHost):
name = "blender"
def install(self):
"""Override install method from HostBase.
Install Blender host functionality."""
install()
def get_containers(self) -> Iterator:
"""List containers from active Blender scene."""
return ls()
def get_workfile_extensions(self) -> List[str]:
"""Override get_workfile_extensions method from IWorkfileHost.
Get workfile possible extensions.
Returns:
List[str]: Workfile extensions.
"""
return file_extensions()
def save_workfile(self, dst_path: str = None):
"""Override save_workfile method from IWorkfileHost.
Save currently opened workfile.
Args:
dst_path (str): Where the current scene should be saved. Or use
current path if `None` is passed.
"""
save_file(dst_path if dst_path else bpy.data.filepath)
def open_workfile(self, filepath: str):
"""Override open_workfile method from IWorkfileHost.
Open workfile at specified filepath in the host.
Args:
filepath (str): Path to workfile.
"""
open_file(filepath)
def get_current_workfile(self) -> str:
"""Override get_current_workfile method from IWorkfileHost.
Retrieve currently opened workfile path.
Returns:
str: Path to currently opened workfile.
"""
return current_file()
def workfile_has_unsaved_changes(self) -> bool:
"""Override wokfile_has_unsaved_changes method from IWorkfileHost.
Returns True if opened workfile has no unsaved changes.
Returns:
bool: True if scene is saved and False if it has unsaved
modifications.
"""
return has_unsaved_changes()
def work_root(self, session) -> str:
"""Override work_root method from IWorkfileHost.
Modify workdir per host.
Args:
session (dict): Session context data.
Returns:
str: Path to new workdir.
"""
return work_root(session)
def get_context_data(self) -> dict:
"""Override abstract method from IPublishHost.
Get global data related to creation-publishing from workfile.
Returns:
dict: Context data stored using 'update_context_data'.
"""
property = bpy.context.scene.get(AVALON_PROPERTY)
if property:
return property.to_dict()
return {}
def update_context_data(self, data: dict, changes: dict):
"""Override abstract method from IPublishHost.
Store global context data to workfile.
Args:
data (dict): New data as are.
changes (dict): Only data that has been changed. Each value has
tuple with '(
The complete documentation is available at this address:https://cfourney.github.io/OpenHarmony/
Install this library to be able to use the scripts that require it.
", "repository": "https://github.com/cfourney/OpenHarmony/", "isPackage": false, "files": [ "openHarmony.js", "openHarmony/" ], "keywords": [ "openHarmony", "library" ], "author": "OpenHarmony", "license": "MPL-2.0", "website": "https://github.com/cfourney/OpenHarmony/", "localFiles": "" }, { "name": "oH Anim tools - Smartkey", "version": "1.0.0", "compatibility": "Harmony Premium 15", "description": "This script creates a key frame on the current timeline layer, and set the stop motion or interpolated mode of the key according to the surrounding keyframes. A Keyframe placed on an interpolation will remain interpolated, and a key placed between stop motion keyframes will also be set to stop motion.\nThis script uses the OpenHarmony library. Install it first to be able to use it.
\n\nAssign this script to a shortcut with the script ScriptShortcuts.
", "repository": "https://github.com/cfourney/OpenHarmony/", "isPackage": false, "files": [ "tools/OpenHarmony_basic/openHarmony_anim_tools.js" ], "keywords": [ "openHarmony", "animation" ], "author": "Chris F", "license": "MPL-v2.0", "website": "https://github.com/cfourney/OpenHarmony/", "localFiles": "" }, { "name": "oH Rigging tools", "version": "1.0.0", "compatibility": "Harmony Premium 15", "description": "OpenHarmony Rigging Tools\nThose scripts require the openHarmony lib to work. Install it first for the scripts to work.
\nAdd Centered Weighted Peg
Adds a peg with a pivot at the center of the selected drawing.
Place Pivot with Click
Place the pivot with a simple click.
Clean Unused Palettes
\nFinds and removes all unnecessary palettes files from the filesystem. Doesn't support Element Palettes yet!
Create Backdrop on Selection
Set up backdrops easily on the selection with this script.
end: raise _DecodeError('Truncated message.') return pos def _SkipGroup(buffer, pos, end): """Skip sub-group. Returns the new position.""" while 1: (tag_bytes, pos) = ReadTag(buffer, pos) new_pos = SkipField(buffer, pos, end, tag_bytes) if new_pos == -1: return pos pos = new_pos def _DecodeUnknownFieldSet(buffer, pos, end_pos=None): """Decode UnknownFieldSet. Returns the UnknownFieldSet and new position.""" unknown_field_set = containers.UnknownFieldSet() while end_pos is None or pos < end_pos: (tag_bytes, pos) = ReadTag(buffer, pos) (tag, _) = _DecodeVarint(tag_bytes, 0) field_number, wire_type = wire_format.UnpackTag(tag) if wire_type == wire_format.WIRETYPE_END_GROUP: break (data, pos) = _DecodeUnknownField(buffer, pos, wire_type) # pylint: disable=protected-access unknown_field_set._add(field_number, wire_type, data) return (unknown_field_set, pos) def _DecodeUnknownField(buffer, pos, wire_type): """Decode a unknown field. Returns the UnknownField and new position.""" if wire_type == wire_format.WIRETYPE_VARINT: (data, pos) = _DecodeVarint(buffer, pos) elif wire_type == wire_format.WIRETYPE_FIXED64: (data, pos) = _DecodeFixed64(buffer, pos) elif wire_type == wire_format.WIRETYPE_FIXED32: (data, pos) = _DecodeFixed32(buffer, pos) elif wire_type == wire_format.WIRETYPE_LENGTH_DELIMITED: (size, pos) = _DecodeVarint(buffer, pos) data = buffer[pos:pos+size].tobytes() pos += size elif wire_type == wire_format.WIRETYPE_START_GROUP: (data, pos) = _DecodeUnknownFieldSet(buffer, pos) elif wire_type == wire_format.WIRETYPE_END_GROUP: return (0, -1) else: raise _DecodeError('Wrong wire type in tag.') return (data, pos) def _EndGroup(buffer, pos, end): """Skipping an END_GROUP tag returns -1 to tell the parent loop to break.""" return -1 def _SkipFixed32(buffer, pos, end): """Skip a fixed32 value. Returns the new position.""" pos += 4 if pos > end: raise _DecodeError('Truncated message.') return pos def _DecodeFixed32(buffer, pos): """Decode a fixed32.""" new_pos = pos + 4 return (struct.unpack('B').pack def EncodeVarint(write, value, unused_deterministic=None): bits = value & 0x7f value >>= 7 while value: write(local_int2byte(0x80|bits)) bits = value & 0x7f value >>= 7 return write(local_int2byte(bits)) return EncodeVarint def _SignedVarintEncoder(): """Return an encoder for a basic signed varint value (does not include tag).""" local_int2byte = struct.Struct('>B').pack def EncodeSignedVarint(write, value, unused_deterministic=None): if value < 0: value += (1 << 64) bits = value & 0x7f value >>= 7 while value: write(local_int2byte(0x80|bits)) bits = value & 0x7f value >>= 7 return write(local_int2byte(bits)) return EncodeSignedVarint _EncodeVarint = _VarintEncoder() _EncodeSignedVarint = _SignedVarintEncoder() def _VarintBytes(value): """Encode the given integer as a varint and return the bytes. This is only called at startup time so it doesn't need to be fast.""" pieces = [] _EncodeVarint(pieces.append, value, True) return b"".join(pieces) def TagBytes(field_number, wire_type): """Encode the given tag and return the bytes. Only called at startup.""" return bytes(_VarintBytes(wire_format.PackTag(field_number, wire_type))) # -------------------------------------------------------------------- # As with sizers (see above), we have a number of common encoder # implementations. def _SimpleEncoder(wire_type, encode_value, compute_value_size): """Return a constructor for an encoder for fields of a particular type. Args: wire_type: The field's wire type, for encoding tags. encode_value: A function which encodes an individual value, e.g. _EncodeVarint(). compute_value_size: A function which computes the size of an individual value, e.g. _VarintSize(). """ def SpecificEncoder(field_number, is_repeated, is_packed): if is_packed: tag_bytes = TagBytes(field_number, wire_format.WIRETYPE_LENGTH_DELIMITED) local_EncodeVarint = _EncodeVarint def EncodePackedField(write, value, deterministic): write(tag_bytes) size = 0 for element in value: size += compute_value_size(element) local_EncodeVarint(write, size, deterministic) for element in value: encode_value(write, element, deterministic) return EncodePackedField elif is_repeated: tag_bytes = TagBytes(field_number, wire_type) def EncodeRepeatedField(write, value, deterministic): for element in value: write(tag_bytes) encode_value(write, element, deterministic) return EncodeRepeatedField else: tag_bytes = TagBytes(field_number, wire_type) def EncodeField(write, value, deterministic): write(tag_bytes) return encode_value(write, value, deterministic) return EncodeField return SpecificEncoder def _ModifiedEncoder(wire_type, encode_value, compute_value_size, modify_value): """Like SimpleEncoder but additionally invokes modify_value on every value before passing it to encode_value. Usually modify_value is ZigZagEncode.""" def SpecificEncoder(field_number, is_repeated, is_packed): if is_packed: tag_bytes = TagBytes(field_number, wire_format.WIRETYPE_LENGTH_DELIMITED) local_EncodeVarint = _EncodeVarint def EncodePackedField(write, value, deterministic): write(tag_bytes) size = 0 for element in value: size += compute_value_size(modify_value(element)) local_EncodeVarint(write, size, deterministic) for element in value: encode_value(write, modify_value(element), deterministic) return EncodePackedField elif is_repeated: tag_bytes = TagBytes(field_number, wire_type) def EncodeRepeatedField(write, value, deterministic): for element in value: write(tag_bytes) encode_value(write, modify_value(element), deterministic) return EncodeRepeatedField else: tag_bytes = TagBytes(field_number, wire_type) def EncodeField(write, value, deterministic): write(tag_bytes) return encode_value(write, modify_value(value), deterministic) return EncodeField return SpecificEncoder def _StructPackEncoder(wire_type, format): """Return a constructor for an encoder for a fixed-width field. Args: wire_type: The field's wire type, for encoding tags. format: The format string to pass to struct.pack(). """ value_size = struct.calcsize(format) def SpecificEncoder(field_number, is_repeated, is_packed): local_struct_pack = struct.pack if is_packed: tag_bytes = TagBytes(field_number, wire_format.WIRETYPE_LENGTH_DELIMITED) local_EncodeVarint = _EncodeVarint def EncodePackedField(write, value, deterministic): write(tag_bytes) local_EncodeVarint(write, len(value) * value_size, deterministic) for element in value: write(local_struct_pack(format, element)) return EncodePackedField elif is_repeated: tag_bytes = TagBytes(field_number, wire_type) def EncodeRepeatedField(write, value, unused_deterministic=None): for element in value: write(tag_bytes) write(local_struct_pack(format, element)) return EncodeRepeatedField else: tag_bytes = TagBytes(field_number, wire_type) def EncodeField(write, value, unused_deterministic=None): write(tag_bytes) return write(local_struct_pack(format, value)) return EncodeField return SpecificEncoder def _FloatingPointEncoder(wire_type, format): """Return a constructor for an encoder for float fields. This is like StructPackEncoder, but catches errors that may be due to passing non-finite floating-point values to struct.pack, and makes a second attempt to encode those values. Args: wire_type: The field's wire type, for encoding tags. format: The format string to pass to struct.pack(). """ value_size = struct.calcsize(format) if value_size == 4: def EncodeNonFiniteOrRaise(write, value): # Remember that the serialized form uses little-endian byte order. if value == _POS_INF: write(b'\x00\x00\x80\x7F') elif value == _NEG_INF: write(b'\x00\x00\x80\xFF') elif value != value: # NaN write(b'\x00\x00\xC0\x7F') else: raise elif value_size == 8: def EncodeNonFiniteOrRaise(write, value): if value == _POS_INF: write(b'\x00\x00\x00\x00\x00\x00\xF0\x7F') elif value == _NEG_INF: write(b'\x00\x00\x00\x00\x00\x00\xF0\xFF') elif value != value: # NaN write(b'\x00\x00\x00\x00\x00\x00\xF8\x7F') else: raise else: raise ValueError('Can\'t encode floating-point values that are ' '%d bytes long (only 4 or 8)' % value_size) def SpecificEncoder(field_number, is_repeated, is_packed): local_struct_pack = struct.pack if is_packed: tag_bytes = TagBytes(field_number, wire_format.WIRETYPE_LENGTH_DELIMITED) local_EncodeVarint = _EncodeVarint def EncodePackedField(write, value, deterministic): write(tag_bytes) local_EncodeVarint(write, len(value) * value_size, deterministic) for element in value: # This try/except block is going to be faster than any code that # we could write to check whether element is finite. try: write(local_struct_pack(format, element)) except SystemError: EncodeNonFiniteOrRaise(write, element) return EncodePackedField elif is_repeated: tag_bytes = TagBytes(field_number, wire_type) def EncodeRepeatedField(write, value, unused_deterministic=None): for element in value: write(tag_bytes) try: write(local_struct_pack(format, element)) except SystemError: EncodeNonFiniteOrRaise(write, element) return EncodeRepeatedField else: tag_bytes = TagBytes(field_number, wire_type) def EncodeField(write, value, unused_deterministic=None): write(tag_bytes) try: write(local_struct_pack(format, value)) except SystemError: EncodeNonFiniteOrRaise(write, value) return EncodeField return SpecificEncoder # ==================================================================== # Here we declare an encoder constructor for each field type. These work # very similarly to sizer constructors, described earlier. Int32Encoder = Int64Encoder = EnumEncoder = _SimpleEncoder( wire_format.WIRETYPE_VARINT, _EncodeSignedVarint, _SignedVarintSize) UInt32Encoder = UInt64Encoder = _SimpleEncoder( wire_format.WIRETYPE_VARINT, _EncodeVarint, _VarintSize) SInt32Encoder = SInt64Encoder = _ModifiedEncoder( wire_format.WIRETYPE_VARINT, _EncodeVarint, _VarintSize, wire_format.ZigZagEncode) # Note that Python conveniently guarantees that when using the '<' prefix on # formats, they will also have the same size across all platforms (as opposed # to without the prefix, where their sizes depend on the C compiler's basic # type sizes). Fixed32Encoder = _StructPackEncoder(wire_format.WIRETYPE_FIXED32, ' str ValueType = int def __init__(self, enum_type): """Inits EnumTypeWrapper with an EnumDescriptor.""" self._enum_type = enum_type self.DESCRIPTOR = enum_type # pylint: disable=invalid-name def Name(self, number): # pylint: disable=invalid-name """Returns a string containing the name of an enum value.""" try: return self._enum_type.values_by_number[number].name except KeyError: pass # fall out to break exception chaining if not isinstance(number, int): raise TypeError( 'Enum value for {} must be an int, but got {} {!r}.'.format( self._enum_type.name, type(number), number)) else: # repr here to handle the odd case when you pass in a boolean. raise ValueError('Enum {} has no name defined for value {!r}'.format( self._enum_type.name, number)) def Value(self, name): # pylint: disable=invalid-name """Returns the value corresponding to the given enum name.""" try: return self._enum_type.values_by_name[name].number except KeyError: pass # fall out to break exception chaining raise ValueError('Enum {} has no value defined for name {!r}'.format( self._enum_type.name, name)) def keys(self): """Return a list of the string names in the enum. Returns: A list of strs, in the order they were defined in the .proto file. """ return [value_descriptor.name for value_descriptor in self._enum_type.values] def values(self): """Return a list of the integer values in the enum. Returns: A list of ints, in the order they were defined in the .proto file. """ return [value_descriptor.number for value_descriptor in self._enum_type.values] def items(self): """Return a list of the (name, value) pairs of the enum. Returns: A list of (str, int) pairs, in the order they were defined in the .proto file. """ return [(value_descriptor.name, value_descriptor.number) for value_descriptor in self._enum_type.values] def __getattr__(self, name): """Returns the value corresponding to the given enum name.""" try: return super( EnumTypeWrapper, self).__getattribute__('_enum_type').values_by_name[name].number except KeyError: pass # fall out to break exception chaining raise AttributeError('Enum {} has no value defined for name {!r}'.format( self._enum_type.name, name)) ================================================ FILE: openpype/hosts/hiero/vendor/google/protobuf/internal/extension_dict.py ================================================ # Protocol Buffers - Google's data interchange format # Copyright 2008 Google Inc. All rights reserved. # https://developers.google.com/protocol-buffers/ # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are # met: # # * Redistributions of source code must retain the above copyright # notice, this list of conditions and the following disclaimer. # * Redistributions in binary form must reproduce the above # copyright notice, this list of conditions and the following disclaimer # in the documentation and/or other materials provided with the # distribution. # * Neither the name of Google Inc. nor the names of its # contributors may be used to endorse or promote products derived from # this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. """Contains _ExtensionDict class to represent extensions. """ from google.protobuf.internal import type_checkers from google.protobuf.descriptor import FieldDescriptor def _VerifyExtensionHandle(message, extension_handle): """Verify that the given extension handle is valid.""" if not isinstance(extension_handle, FieldDescriptor): raise KeyError('HasExtension() expects an extension handle, got: %s' % extension_handle) if not extension_handle.is_extension: raise KeyError('"%s" is not an extension.' % extension_handle.full_name) if not extension_handle.containing_type: raise KeyError('"%s" is missing a containing_type.' % extension_handle.full_name) if extension_handle.containing_type is not message.DESCRIPTOR: raise KeyError('Extension "%s" extends message type "%s", but this ' 'message is of type "%s".' % (extension_handle.full_name, extension_handle.containing_type.full_name, message.DESCRIPTOR.full_name)) # TODO(robinson): Unify error handling of "unknown extension" crap. # TODO(robinson): Support iteritems()-style iteration over all # extensions with the "has" bits turned on? class _ExtensionDict(object): """Dict-like container for Extension fields on proto instances. Note that in all cases we expect extension handles to be FieldDescriptors. """ def __init__(self, extended_message): """ Args: extended_message: Message instance for which we are the Extensions dict. """ self._extended_message = extended_message def __getitem__(self, extension_handle): """Returns the current value of the given extension handle.""" _VerifyExtensionHandle(self._extended_message, extension_handle) result = self._extended_message._fields.get(extension_handle) if result is not None: return result if extension_handle.label == FieldDescriptor.LABEL_REPEATED: result = extension_handle._default_constructor(self._extended_message) elif extension_handle.cpp_type == FieldDescriptor.CPPTYPE_MESSAGE: message_type = extension_handle.message_type if not hasattr(message_type, '_concrete_class'): # pylint: disable=protected-access self._extended_message._FACTORY.GetPrototype(message_type) assert getattr(extension_handle.message_type, '_concrete_class', None), ( 'Uninitialized concrete class found for field %r (message type %r)' % (extension_handle.full_name, extension_handle.message_type.full_name)) result = extension_handle.message_type._concrete_class() try: result._SetListener(self._extended_message._listener_for_children) except ReferenceError: pass else: # Singular scalar -- just return the default without inserting into the # dict. return extension_handle.default_value # Atomically check if another thread has preempted us and, if not, swap # in the new object we just created. If someone has preempted us, we # take that object and discard ours. # WARNING: We are relying on setdefault() being atomic. This is true # in CPython but we haven't investigated others. This warning appears # in several other locations in this file. result = self._extended_message._fields.setdefault( extension_handle, result) return result def __eq__(self, other): if not isinstance(other, self.__class__): return False my_fields = self._extended_message.ListFields() other_fields = other._extended_message.ListFields() # Get rid of non-extension fields. my_fields = [field for field in my_fields if field.is_extension] other_fields = [field for field in other_fields if field.is_extension] return my_fields == other_fields def __ne__(self, other): return not self == other def __len__(self): fields = self._extended_message.ListFields() # Get rid of non-extension fields. extension_fields = [field for field in fields if field[0].is_extension] return len(extension_fields) def __hash__(self): raise TypeError('unhashable object') # Note that this is only meaningful for non-repeated, scalar extension # fields. Note also that we may have to call _Modified() when we do # successfully set a field this way, to set any necessary "has" bits in the # ancestors of the extended message. def __setitem__(self, extension_handle, value): """If extension_handle specifies a non-repeated, scalar extension field, sets the value of that field. """ _VerifyExtensionHandle(self._extended_message, extension_handle) if (extension_handle.label == FieldDescriptor.LABEL_REPEATED or extension_handle.cpp_type == FieldDescriptor.CPPTYPE_MESSAGE): raise TypeError( 'Cannot assign to extension "%s" because it is a repeated or ' 'composite type.' % extension_handle.full_name) # It's slightly wasteful to lookup the type checker each time, # but we expect this to be a vanishingly uncommon case anyway. type_checker = type_checkers.GetTypeChecker(extension_handle) # pylint: disable=protected-access self._extended_message._fields[extension_handle] = ( type_checker.CheckValue(value)) self._extended_message._Modified() def __delitem__(self, extension_handle): self._extended_message.ClearExtension(extension_handle) def _FindExtensionByName(self, name): """Tries to find a known extension with the specified name. Args: name: Extension full name. Returns: Extension field descriptor. """ return self._extended_message._extensions_by_name.get(name, None) def _FindExtensionByNumber(self, number): """Tries to find a known extension with the field number. Args: number: Extension field number. Returns: Extension field descriptor. """ return self._extended_message._extensions_by_number.get(number, None) def __iter__(self): # Return a generator over the populated extension fields return (f[0] for f in self._extended_message.ListFields() if f[0].is_extension) def __contains__(self, extension_handle): _VerifyExtensionHandle(self._extended_message, extension_handle) if extension_handle not in self._extended_message._fields: return False if extension_handle.label == FieldDescriptor.LABEL_REPEATED: return bool(self._extended_message._fields.get(extension_handle)) if extension_handle.cpp_type == FieldDescriptor.CPPTYPE_MESSAGE: value = self._extended_message._fields.get(extension_handle) # pylint: disable=protected-access return value is not None and value._is_present_in_parent return True ================================================ FILE: openpype/hosts/hiero/vendor/google/protobuf/internal/message_listener.py ================================================ # Protocol Buffers - Google's data interchange format # Copyright 2008 Google Inc. All rights reserved. # https://developers.google.com/protocol-buffers/ # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are # met: # # * Redistributions of source code must retain the above copyright # notice, this list of conditions and the following disclaimer. # * Redistributions in binary form must reproduce the above # copyright notice, this list of conditions and the following disclaimer # in the documentation and/or other materials provided with the # distribution. # * Neither the name of Google Inc. nor the names of its # contributors may be used to endorse or promote products derived from # this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. """Defines a listener interface for observing certain state transitions on Message objects. Also defines a null implementation of this interface. """ __author__ = 'robinson@google.com (Will Robinson)' class MessageListener(object): """Listens for modifications made to a message. Meant to be registered via Message._SetListener(). Attributes: dirty: If True, then calling Modified() would be a no-op. This can be used to avoid these calls entirely in the common case. """ def Modified(self): """Called every time the message is modified in such a way that the parent message may need to be updated. This currently means either: (a) The message was modified for the first time, so the parent message should henceforth mark the message as present. (b) The message's cached byte size became dirty -- i.e. the message was modified for the first time after a previous call to ByteSize(). Therefore the parent should also mark its byte size as dirty. Note that (a) implies (b), since new objects start out with a client cached size (zero). However, we document (a) explicitly because it is important. Modified() will *only* be called in response to one of these two events -- not every time the sub-message is modified. Note that if the listener's |dirty| attribute is true, then calling Modified at the moment would be a no-op, so it can be skipped. Performance- sensitive callers should check this attribute directly before calling since it will be true most of the time. """ raise NotImplementedError class NullMessageListener(object): """No-op MessageListener implementation.""" def Modified(self): pass ================================================ FILE: openpype/hosts/hiero/vendor/google/protobuf/internal/message_set_extensions_pb2.py ================================================ # -*- coding: utf-8 -*- # Generated by the protocol buffer compiler. DO NOT EDIT! # source: google/protobuf/internal/message_set_extensions.proto """Generated protocol buffer code.""" from google.protobuf.internal import builder as _builder from google.protobuf import descriptor as _descriptor from google.protobuf import descriptor_pool as _descriptor_pool from google.protobuf import symbol_database as _symbol_database # @@protoc_insertion_point(imports) _sym_db = _symbol_database.Default() DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n5google/protobuf/internal/message_set_extensions.proto\x12\x18google.protobuf.internal\"\x1e\n\x0eTestMessageSet*\x08\x08\x04\x10\xff\xff\xff\xff\x07:\x02\x08\x01\"\xa5\x01\n\x18TestMessageSetExtension1\x12\t\n\x01i\x18\x0f \x01(\x05\x32~\n\x15message_set_extension\x12(.google.protobuf.internal.TestMessageSet\x18\xab\xff\xf6. \x01(\x0b\x32\x32.google.protobuf.internal.TestMessageSetExtension1\"\xa7\x01\n\x18TestMessageSetExtension2\x12\x0b\n\x03str\x18\x19 \x01(\t2~\n\x15message_set_extension\x12(.google.protobuf.internal.TestMessageSet\x18\xca\xff\xf6. \x01(\x0b\x32\x32.google.protobuf.internal.TestMessageSetExtension2\"(\n\x18TestMessageSetExtension3\x12\x0c\n\x04text\x18# \x01(\t:\x7f\n\x16message_set_extension3\x12(.google.protobuf.internal.TestMessageSet\x18\xdf\xff\xf6. \x01(\x0b\x32\x32.google.protobuf.internal.TestMessageSetExtension3') _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals()) _builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'google.protobuf.internal.message_set_extensions_pb2', globals()) if _descriptor._USE_C_DESCRIPTORS == False: TestMessageSet.RegisterExtension(message_set_extension3) TestMessageSet.RegisterExtension(_TESTMESSAGESETEXTENSION1.extensions_by_name['message_set_extension']) TestMessageSet.RegisterExtension(_TESTMESSAGESETEXTENSION2.extensions_by_name['message_set_extension']) DESCRIPTOR._options = None _TESTMESSAGESET._options = None _TESTMESSAGESET._serialized_options = b'\010\001' _TESTMESSAGESET._serialized_start=83 _TESTMESSAGESET._serialized_end=113 _TESTMESSAGESETEXTENSION1._serialized_start=116 _TESTMESSAGESETEXTENSION1._serialized_end=281 _TESTMESSAGESETEXTENSION2._serialized_start=284 _TESTMESSAGESETEXTENSION2._serialized_end=451 _TESTMESSAGESETEXTENSION3._serialized_start=453 _TESTMESSAGESETEXTENSION3._serialized_end=493 # @@protoc_insertion_point(module_scope) ================================================ FILE: openpype/hosts/hiero/vendor/google/protobuf/internal/missing_enum_values_pb2.py ================================================ # -*- coding: utf-8 -*- # Generated by the protocol buffer compiler. DO NOT EDIT! # source: google/protobuf/internal/missing_enum_values.proto """Generated protocol buffer code.""" from google.protobuf.internal import builder as _builder from google.protobuf import descriptor as _descriptor from google.protobuf import descriptor_pool as _descriptor_pool from google.protobuf import symbol_database as _symbol_database # @@protoc_insertion_point(imports) _sym_db = _symbol_database.Default() DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n2google/protobuf/internal/missing_enum_values.proto\x12\x1fgoogle.protobuf.python.internal\"\xc1\x02\n\x0eTestEnumValues\x12X\n\x14optional_nested_enum\x18\x01 \x01(\x0e\x32:.google.protobuf.python.internal.TestEnumValues.NestedEnum\x12X\n\x14repeated_nested_enum\x18\x02 \x03(\x0e\x32:.google.protobuf.python.internal.TestEnumValues.NestedEnum\x12Z\n\x12packed_nested_enum\x18\x03 \x03(\x0e\x32:.google.protobuf.python.internal.TestEnumValues.NestedEnumB\x02\x10\x01\"\x1f\n\nNestedEnum\x12\x08\n\x04ZERO\x10\x00\x12\x07\n\x03ONE\x10\x01\"\xd3\x02\n\x15TestMissingEnumValues\x12_\n\x14optional_nested_enum\x18\x01 \x01(\x0e\x32\x41.google.protobuf.python.internal.TestMissingEnumValues.NestedEnum\x12_\n\x14repeated_nested_enum\x18\x02 \x03(\x0e\x32\x41.google.protobuf.python.internal.TestMissingEnumValues.NestedEnum\x12\x61\n\x12packed_nested_enum\x18\x03 \x03(\x0e\x32\x41.google.protobuf.python.internal.TestMissingEnumValues.NestedEnumB\x02\x10\x01\"\x15\n\nNestedEnum\x12\x07\n\x03TWO\x10\x02\"\x1b\n\nJustString\x12\r\n\x05\x64ummy\x18\x01 \x02(\t') _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals()) _builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'google.protobuf.internal.missing_enum_values_pb2', globals()) if _descriptor._USE_C_DESCRIPTORS == False: DESCRIPTOR._options = None _TESTENUMVALUES.fields_by_name['packed_nested_enum']._options = None _TESTENUMVALUES.fields_by_name['packed_nested_enum']._serialized_options = b'\020\001' _TESTMISSINGENUMVALUES.fields_by_name['packed_nested_enum']._options = None _TESTMISSINGENUMVALUES.fields_by_name['packed_nested_enum']._serialized_options = b'\020\001' _TESTENUMVALUES._serialized_start=88 _TESTENUMVALUES._serialized_end=409 _TESTENUMVALUES_NESTEDENUM._serialized_start=378 _TESTENUMVALUES_NESTEDENUM._serialized_end=409 _TESTMISSINGENUMVALUES._serialized_start=412 _TESTMISSINGENUMVALUES._serialized_end=751 _TESTMISSINGENUMVALUES_NESTEDENUM._serialized_start=730 _TESTMISSINGENUMVALUES_NESTEDENUM._serialized_end=751 _JUSTSTRING._serialized_start=753 _JUSTSTRING._serialized_end=780 # @@protoc_insertion_point(module_scope) ================================================ FILE: openpype/hosts/hiero/vendor/google/protobuf/internal/more_extensions_dynamic_pb2.py ================================================ # -*- coding: utf-8 -*- # Generated by the protocol buffer compiler. DO NOT EDIT! # source: google/protobuf/internal/more_extensions_dynamic.proto """Generated protocol buffer code.""" from google.protobuf.internal import builder as _builder from google.protobuf import descriptor as _descriptor from google.protobuf import descriptor_pool as _descriptor_pool from google.protobuf import symbol_database as _symbol_database # @@protoc_insertion_point(imports) _sym_db = _symbol_database.Default() from google.protobuf.internal import more_extensions_pb2 as google_dot_protobuf_dot_internal_dot_more__extensions__pb2 DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n6google/protobuf/internal/more_extensions_dynamic.proto\x12\x18google.protobuf.internal\x1a.google/protobuf/internal/more_extensions.proto\"\x1f\n\x12\x44ynamicMessageType\x12\t\n\x01\x61\x18\x01 \x01(\x05:J\n\x17\x64ynamic_int32_extension\x12).google.protobuf.internal.ExtendedMessage\x18\x64 \x01(\x05:z\n\x19\x64ynamic_message_extension\x12).google.protobuf.internal.ExtendedMessage\x18\x65 \x01(\x0b\x32,.google.protobuf.internal.DynamicMessageType:\x83\x01\n\"repeated_dynamic_message_extension\x12).google.protobuf.internal.ExtendedMessage\x18\x66 \x03(\x0b\x32,.google.protobuf.internal.DynamicMessageType') _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals()) _builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'google.protobuf.internal.more_extensions_dynamic_pb2', globals()) if _descriptor._USE_C_DESCRIPTORS == False: google_dot_protobuf_dot_internal_dot_more__extensions__pb2.ExtendedMessage.RegisterExtension(dynamic_int32_extension) google_dot_protobuf_dot_internal_dot_more__extensions__pb2.ExtendedMessage.RegisterExtension(dynamic_message_extension) google_dot_protobuf_dot_internal_dot_more__extensions__pb2.ExtendedMessage.RegisterExtension(repeated_dynamic_message_extension) DESCRIPTOR._options = None _DYNAMICMESSAGETYPE._serialized_start=132 _DYNAMICMESSAGETYPE._serialized_end=163 # @@protoc_insertion_point(module_scope) ================================================ FILE: openpype/hosts/hiero/vendor/google/protobuf/internal/more_extensions_pb2.py ================================================ # -*- coding: utf-8 -*- # Generated by the protocol buffer compiler. DO NOT EDIT! # source: google/protobuf/internal/more_extensions.proto """Generated protocol buffer code.""" from google.protobuf.internal import builder as _builder from google.protobuf import descriptor as _descriptor from google.protobuf import descriptor_pool as _descriptor_pool from google.protobuf import symbol_database as _symbol_database # @@protoc_insertion_point(imports) _sym_db = _symbol_database.Default() DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n.google/protobuf/internal/more_extensions.proto\x12\x18google.protobuf.internal\"\x99\x01\n\x0fTopLevelMessage\x12\x41\n\nsubmessage\x18\x01 \x01(\x0b\x32).google.protobuf.internal.ExtendedMessageB\x02(\x01\x12\x43\n\x0enested_message\x18\x02 \x01(\x0b\x32\'.google.protobuf.internal.NestedMessageB\x02(\x01\"R\n\rNestedMessage\x12\x41\n\nsubmessage\x18\x01 \x01(\x0b\x32).google.protobuf.internal.ExtendedMessageB\x02(\x01\"K\n\x0f\x45xtendedMessage\x12\x17\n\x0eoptional_int32\x18\xe9\x07 \x01(\x05\x12\x18\n\x0frepeated_string\x18\xea\x07 \x03(\t*\x05\x08\x01\x10\xe8\x07\"-\n\x0e\x46oreignMessage\x12\x1b\n\x13\x66oreign_message_int\x18\x01 \x01(\x05:I\n\x16optional_int_extension\x12).google.protobuf.internal.ExtendedMessage\x18\x01 \x01(\x05:w\n\x1aoptional_message_extension\x12).google.protobuf.internal.ExtendedMessage\x18\x02 \x01(\x0b\x32(.google.protobuf.internal.ForeignMessage:I\n\x16repeated_int_extension\x12).google.protobuf.internal.ExtendedMessage\x18\x03 \x03(\x05:w\n\x1arepeated_message_extension\x12).google.protobuf.internal.ExtendedMessage\x18\x04 \x03(\x0b\x32(.google.protobuf.internal.ForeignMessage') _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals()) _builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'google.protobuf.internal.more_extensions_pb2', globals()) if _descriptor._USE_C_DESCRIPTORS == False: ExtendedMessage.RegisterExtension(optional_int_extension) ExtendedMessage.RegisterExtension(optional_message_extension) ExtendedMessage.RegisterExtension(repeated_int_extension) ExtendedMessage.RegisterExtension(repeated_message_extension) DESCRIPTOR._options = None _TOPLEVELMESSAGE.fields_by_name['submessage']._options = None _TOPLEVELMESSAGE.fields_by_name['submessage']._serialized_options = b'(\001' _TOPLEVELMESSAGE.fields_by_name['nested_message']._options = None _TOPLEVELMESSAGE.fields_by_name['nested_message']._serialized_options = b'(\001' _NESTEDMESSAGE.fields_by_name['submessage']._options = None _NESTEDMESSAGE.fields_by_name['submessage']._serialized_options = b'(\001' _TOPLEVELMESSAGE._serialized_start=77 _TOPLEVELMESSAGE._serialized_end=230 _NESTEDMESSAGE._serialized_start=232 _NESTEDMESSAGE._serialized_end=314 _EXTENDEDMESSAGE._serialized_start=316 _EXTENDEDMESSAGE._serialized_end=391 _FOREIGNMESSAGE._serialized_start=393 _FOREIGNMESSAGE._serialized_end=438 # @@protoc_insertion_point(module_scope) ================================================ FILE: openpype/hosts/hiero/vendor/google/protobuf/internal/more_messages_pb2.py ================================================ # -*- coding: utf-8 -*- # Generated by the protocol buffer compiler. DO NOT EDIT! # source: google/protobuf/internal/more_messages.proto """Generated protocol buffer code.""" from google.protobuf.internal import builder as _builder from google.protobuf import descriptor as _descriptor from google.protobuf import descriptor_pool as _descriptor_pool from google.protobuf import symbol_database as _symbol_database # @@protoc_insertion_point(imports) _sym_db = _symbol_database.Default() DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n,google/protobuf/internal/more_messages.proto\x12\x18google.protobuf.internal\"h\n\x10OutOfOrderFields\x12\x17\n\x0foptional_sint32\x18\x05 \x01(\x11\x12\x17\n\x0foptional_uint32\x18\x03 \x01(\r\x12\x16\n\x0eoptional_int32\x18\x01 \x01(\x05*\x04\x08\x04\x10\x05*\x04\x08\x02\x10\x03\"\xcd\x02\n\x05\x63lass\x12\x1b\n\tint_field\x18\x01 \x01(\x05R\x08json_int\x12\n\n\x02if\x18\x02 \x01(\x05\x12(\n\x02\x61s\x18\x03 \x01(\x0e\x32\x1c.google.protobuf.internal.is\x12\x30\n\nenum_field\x18\x04 \x01(\x0e\x32\x1c.google.protobuf.internal.is\x12>\n\x11nested_enum_field\x18\x05 \x01(\x0e\x32#.google.protobuf.internal.class.for\x12;\n\x0enested_message\x18\x06 \x01(\x0b\x32#.google.protobuf.internal.class.try\x1a\x1c\n\x03try\x12\r\n\x05\x66ield\x18\x01 \x01(\x05*\x06\x08\xe7\x07\x10\x90N\"\x1c\n\x03\x66or\x12\x0b\n\x07\x64\x65\x66\x61ult\x10\x00\x12\x08\n\x04True\x10\x01*\x06\x08\xe7\x07\x10\x90N\"?\n\x0b\x45xtendClass20\n\x06return\x12\x1f.google.protobuf.internal.class\x18\xea\x07 \x01(\x05\"~\n\x0fTestFullKeyword\x12:\n\x06\x66ield1\x18\x01 \x01(\x0b\x32*.google.protobuf.internal.OutOfOrderFields\x12/\n\x06\x66ield2\x18\x02 \x01(\x0b\x32\x1f.google.protobuf.internal.class\"\xa5\x0f\n\x11LotsNestedMessage\x1a\x04\n\x02\x42\x30\x1a\x04\n\x02\x42\x31\x1a\x04\n\x02\x42\x32\x1a\x04\n\x02\x42\x33\x1a\x04\n\x02\x42\x34\x1a\x04\n\x02\x42\x35\x1a\x04\n\x02\x42\x36\x1a\x04\n\x02\x42\x37\x1a\x04\n\x02\x42\x38\x1a\x04\n\x02\x42\x39\x1a\x05\n\x03\x42\x31\x30\x1a\x05\n\x03\x42\x31\x31\x1a\x05\n\x03\x42\x31\x32\x1a\x05\n\x03\x42\x31\x33\x1a\x05\n\x03\x42\x31\x34\x1a\x05\n\x03\x42\x31\x35\x1a\x05\n\x03\x42\x31\x36\x1a\x05\n\x03\x42\x31\x37\x1a\x05\n\x03\x42\x31\x38\x1a\x05\n\x03\x42\x31\x39\x1a\x05\n\x03\x42\x32\x30\x1a\x05\n\x03\x42\x32\x31\x1a\x05\n\x03\x42\x32\x32\x1a\x05\n\x03\x42\x32\x33\x1a\x05\n\x03\x42\x32\x34\x1a\x05\n\x03\x42\x32\x35\x1a\x05\n\x03\x42\x32\x36\x1a\x05\n\x03\x42\x32\x37\x1a\x05\n\x03\x42\x32\x38\x1a\x05\n\x03\x42\x32\x39\x1a\x05\n\x03\x42\x33\x30\x1a\x05\n\x03\x42\x33\x31\x1a\x05\n\x03\x42\x33\x32\x1a\x05\n\x03\x42\x33\x33\x1a\x05\n\x03\x42\x33\x34\x1a\x05\n\x03\x42\x33\x35\x1a\x05\n\x03\x42\x33\x36\x1a\x05\n\x03\x42\x33\x37\x1a\x05\n\x03\x42\x33\x38\x1a\x05\n\x03\x42\x33\x39\x1a\x05\n\x03\x42\x34\x30\x1a\x05\n\x03\x42\x34\x31\x1a\x05\n\x03\x42\x34\x32\x1a\x05\n\x03\x42\x34\x33\x1a\x05\n\x03\x42\x34\x34\x1a\x05\n\x03\x42\x34\x35\x1a\x05\n\x03\x42\x34\x36\x1a\x05\n\x03\x42\x34\x37\x1a\x05\n\x03\x42\x34\x38\x1a\x05\n\x03\x42\x34\x39\x1a\x05\n\x03\x42\x35\x30\x1a\x05\n\x03\x42\x35\x31\x1a\x05\n\x03\x42\x35\x32\x1a\x05\n\x03\x42\x35\x33\x1a\x05\n\x03\x42\x35\x34\x1a\x05\n\x03\x42\x35\x35\x1a\x05\n\x03\x42\x35\x36\x1a\x05\n\x03\x42\x35\x37\x1a\x05\n\x03\x42\x35\x38\x1a\x05\n\x03\x42\x35\x39\x1a\x05\n\x03\x42\x36\x30\x1a\x05\n\x03\x42\x36\x31\x1a\x05\n\x03\x42\x36\x32\x1a\x05\n\x03\x42\x36\x33\x1a\x05\n\x03\x42\x36\x34\x1a\x05\n\x03\x42\x36\x35\x1a\x05\n\x03\x42\x36\x36\x1a\x05\n\x03\x42\x36\x37\x1a\x05\n\x03\x42\x36\x38\x1a\x05\n\x03\x42\x36\x39\x1a\x05\n\x03\x42\x37\x30\x1a\x05\n\x03\x42\x37\x31\x1a\x05\n\x03\x42\x37\x32\x1a\x05\n\x03\x42\x37\x33\x1a\x05\n\x03\x42\x37\x34\x1a\x05\n\x03\x42\x37\x35\x1a\x05\n\x03\x42\x37\x36\x1a\x05\n\x03\x42\x37\x37\x1a\x05\n\x03\x42\x37\x38\x1a\x05\n\x03\x42\x37\x39\x1a\x05\n\x03\x42\x38\x30\x1a\x05\n\x03\x42\x38\x31\x1a\x05\n\x03\x42\x38\x32\x1a\x05\n\x03\x42\x38\x33\x1a\x05\n\x03\x42\x38\x34\x1a\x05\n\x03\x42\x38\x35\x1a\x05\n\x03\x42\x38\x36\x1a\x05\n\x03\x42\x38\x37\x1a\x05\n\x03\x42\x38\x38\x1a\x05\n\x03\x42\x38\x39\x1a\x05\n\x03\x42\x39\x30\x1a\x05\n\x03\x42\x39\x31\x1a\x05\n\x03\x42\x39\x32\x1a\x05\n\x03\x42\x39\x33\x1a\x05\n\x03\x42\x39\x34\x1a\x05\n\x03\x42\x39\x35\x1a\x05\n\x03\x42\x39\x36\x1a\x05\n\x03\x42\x39\x37\x1a\x05\n\x03\x42\x39\x38\x1a\x05\n\x03\x42\x39\x39\x1a\x06\n\x04\x42\x31\x30\x30\x1a\x06\n\x04\x42\x31\x30\x31\x1a\x06\n\x04\x42\x31\x30\x32\x1a\x06\n\x04\x42\x31\x30\x33\x1a\x06\n\x04\x42\x31\x30\x34\x1a\x06\n\x04\x42\x31\x30\x35\x1a\x06\n\x04\x42\x31\x30\x36\x1a\x06\n\x04\x42\x31\x30\x37\x1a\x06\n\x04\x42\x31\x30\x38\x1a\x06\n\x04\x42\x31\x30\x39\x1a\x06\n\x04\x42\x31\x31\x30\x1a\x06\n\x04\x42\x31\x31\x31\x1a\x06\n\x04\x42\x31\x31\x32\x1a\x06\n\x04\x42\x31\x31\x33\x1a\x06\n\x04\x42\x31\x31\x34\x1a\x06\n\x04\x42\x31\x31\x35\x1a\x06\n\x04\x42\x31\x31\x36\x1a\x06\n\x04\x42\x31\x31\x37\x1a\x06\n\x04\x42\x31\x31\x38\x1a\x06\n\x04\x42\x31\x31\x39\x1a\x06\n\x04\x42\x31\x32\x30\x1a\x06\n\x04\x42\x31\x32\x31\x1a\x06\n\x04\x42\x31\x32\x32\x1a\x06\n\x04\x42\x31\x32\x33\x1a\x06\n\x04\x42\x31\x32\x34\x1a\x06\n\x04\x42\x31\x32\x35\x1a\x06\n\x04\x42\x31\x32\x36\x1a\x06\n\x04\x42\x31\x32\x37\x1a\x06\n\x04\x42\x31\x32\x38\x1a\x06\n\x04\x42\x31\x32\x39\x1a\x06\n\x04\x42\x31\x33\x30\x1a\x06\n\x04\x42\x31\x33\x31\x1a\x06\n\x04\x42\x31\x33\x32\x1a\x06\n\x04\x42\x31\x33\x33\x1a\x06\n\x04\x42\x31\x33\x34\x1a\x06\n\x04\x42\x31\x33\x35\x1a\x06\n\x04\x42\x31\x33\x36\x1a\x06\n\x04\x42\x31\x33\x37\x1a\x06\n\x04\x42\x31\x33\x38\x1a\x06\n\x04\x42\x31\x33\x39\x1a\x06\n\x04\x42\x31\x34\x30\x1a\x06\n\x04\x42\x31\x34\x31\x1a\x06\n\x04\x42\x31\x34\x32\x1a\x06\n\x04\x42\x31\x34\x33\x1a\x06\n\x04\x42\x31\x34\x34\x1a\x06\n\x04\x42\x31\x34\x35\x1a\x06\n\x04\x42\x31\x34\x36\x1a\x06\n\x04\x42\x31\x34\x37\x1a\x06\n\x04\x42\x31\x34\x38\x1a\x06\n\x04\x42\x31\x34\x39\x1a\x06\n\x04\x42\x31\x35\x30\x1a\x06\n\x04\x42\x31\x35\x31\x1a\x06\n\x04\x42\x31\x35\x32\x1a\x06\n\x04\x42\x31\x35\x33\x1a\x06\n\x04\x42\x31\x35\x34\x1a\x06\n\x04\x42\x31\x35\x35\x1a\x06\n\x04\x42\x31\x35\x36\x1a\x06\n\x04\x42\x31\x35\x37\x1a\x06\n\x04\x42\x31\x35\x38\x1a\x06\n\x04\x42\x31\x35\x39\x1a\x06\n\x04\x42\x31\x36\x30\x1a\x06\n\x04\x42\x31\x36\x31\x1a\x06\n\x04\x42\x31\x36\x32\x1a\x06\n\x04\x42\x31\x36\x33\x1a\x06\n\x04\x42\x31\x36\x34\x1a\x06\n\x04\x42\x31\x36\x35\x1a\x06\n\x04\x42\x31\x36\x36\x1a\x06\n\x04\x42\x31\x36\x37\x1a\x06\n\x04\x42\x31\x36\x38\x1a\x06\n\x04\x42\x31\x36\x39\x1a\x06\n\x04\x42\x31\x37\x30\x1a\x06\n\x04\x42\x31\x37\x31\x1a\x06\n\x04\x42\x31\x37\x32\x1a\x06\n\x04\x42\x31\x37\x33\x1a\x06\n\x04\x42\x31\x37\x34\x1a\x06\n\x04\x42\x31\x37\x35\x1a\x06\n\x04\x42\x31\x37\x36\x1a\x06\n\x04\x42\x31\x37\x37\x1a\x06\n\x04\x42\x31\x37\x38\x1a\x06\n\x04\x42\x31\x37\x39\x1a\x06\n\x04\x42\x31\x38\x30\x1a\x06\n\x04\x42\x31\x38\x31\x1a\x06\n\x04\x42\x31\x38\x32\x1a\x06\n\x04\x42\x31\x38\x33\x1a\x06\n\x04\x42\x31\x38\x34\x1a\x06\n\x04\x42\x31\x38\x35\x1a\x06\n\x04\x42\x31\x38\x36\x1a\x06\n\x04\x42\x31\x38\x37\x1a\x06\n\x04\x42\x31\x38\x38\x1a\x06\n\x04\x42\x31\x38\x39\x1a\x06\n\x04\x42\x31\x39\x30\x1a\x06\n\x04\x42\x31\x39\x31\x1a\x06\n\x04\x42\x31\x39\x32\x1a\x06\n\x04\x42\x31\x39\x33\x1a\x06\n\x04\x42\x31\x39\x34\x1a\x06\n\x04\x42\x31\x39\x35\x1a\x06\n\x04\x42\x31\x39\x36\x1a\x06\n\x04\x42\x31\x39\x37\x1a\x06\n\x04\x42\x31\x39\x38\x1a\x06\n\x04\x42\x31\x39\x39\x1a\x06\n\x04\x42\x32\x30\x30\x1a\x06\n\x04\x42\x32\x30\x31\x1a\x06\n\x04\x42\x32\x30\x32\x1a\x06\n\x04\x42\x32\x30\x33\x1a\x06\n\x04\x42\x32\x30\x34\x1a\x06\n\x04\x42\x32\x30\x35\x1a\x06\n\x04\x42\x32\x30\x36\x1a\x06\n\x04\x42\x32\x30\x37\x1a\x06\n\x04\x42\x32\x30\x38\x1a\x06\n\x04\x42\x32\x30\x39\x1a\x06\n\x04\x42\x32\x31\x30\x1a\x06\n\x04\x42\x32\x31\x31\x1a\x06\n\x04\x42\x32\x31\x32\x1a\x06\n\x04\x42\x32\x31\x33\x1a\x06\n\x04\x42\x32\x31\x34\x1a\x06\n\x04\x42\x32\x31\x35\x1a\x06\n\x04\x42\x32\x31\x36\x1a\x06\n\x04\x42\x32\x31\x37\x1a\x06\n\x04\x42\x32\x31\x38\x1a\x06\n\x04\x42\x32\x31\x39\x1a\x06\n\x04\x42\x32\x32\x30\x1a\x06\n\x04\x42\x32\x32\x31\x1a\x06\n\x04\x42\x32\x32\x32\x1a\x06\n\x04\x42\x32\x32\x33\x1a\x06\n\x04\x42\x32\x32\x34\x1a\x06\n\x04\x42\x32\x32\x35\x1a\x06\n\x04\x42\x32\x32\x36\x1a\x06\n\x04\x42\x32\x32\x37\x1a\x06\n\x04\x42\x32\x32\x38\x1a\x06\n\x04\x42\x32\x32\x39\x1a\x06\n\x04\x42\x32\x33\x30\x1a\x06\n\x04\x42\x32\x33\x31\x1a\x06\n\x04\x42\x32\x33\x32\x1a\x06\n\x04\x42\x32\x33\x33\x1a\x06\n\x04\x42\x32\x33\x34\x1a\x06\n\x04\x42\x32\x33\x35\x1a\x06\n\x04\x42\x32\x33\x36\x1a\x06\n\x04\x42\x32\x33\x37\x1a\x06\n\x04\x42\x32\x33\x38\x1a\x06\n\x04\x42\x32\x33\x39\x1a\x06\n\x04\x42\x32\x34\x30\x1a\x06\n\x04\x42\x32\x34\x31\x1a\x06\n\x04\x42\x32\x34\x32\x1a\x06\n\x04\x42\x32\x34\x33\x1a\x06\n\x04\x42\x32\x34\x34\x1a\x06\n\x04\x42\x32\x34\x35\x1a\x06\n\x04\x42\x32\x34\x36\x1a\x06\n\x04\x42\x32\x34\x37\x1a\x06\n\x04\x42\x32\x34\x38\x1a\x06\n\x04\x42\x32\x34\x39\x1a\x06\n\x04\x42\x32\x35\x30\x1a\x06\n\x04\x42\x32\x35\x31\x1a\x06\n\x04\x42\x32\x35\x32\x1a\x06\n\x04\x42\x32\x35\x33\x1a\x06\n\x04\x42\x32\x35\x34\x1a\x06\n\x04\x42\x32\x35\x35*\x1b\n\x02is\x12\x0b\n\x07\x64\x65\x66\x61ult\x10\x00\x12\x08\n\x04\x65lse\x10\x01:C\n\x0foptional_uint64\x12*.google.protobuf.internal.OutOfOrderFields\x18\x04 \x01(\x04:B\n\x0eoptional_int64\x12*.google.protobuf.internal.OutOfOrderFields\x18\x02 \x01(\x03:2\n\x08\x63ontinue\x12\x1f.google.protobuf.internal.class\x18\xe9\x07 \x01(\x05:2\n\x04with\x12#.google.protobuf.internal.class.try\x18\xe9\x07 \x01(\x05') _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals()) _builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'google.protobuf.internal.more_messages_pb2', globals()) if _descriptor._USE_C_DESCRIPTORS == False: OutOfOrderFields.RegisterExtension(optional_uint64) OutOfOrderFields.RegisterExtension(optional_int64) globals()['class'].RegisterExtension(globals()['continue']) getattr(globals()['class'], 'try').RegisterExtension(globals()['with']) globals()['class'].RegisterExtension(_EXTENDCLASS.extensions_by_name['return']) DESCRIPTOR._options = None _IS._serialized_start=2669 _IS._serialized_end=2696 _OUTOFORDERFIELDS._serialized_start=74 _OUTOFORDERFIELDS._serialized_end=178 _CLASS._serialized_start=181 _CLASS._serialized_end=514 _CLASS_TRY._serialized_start=448 _CLASS_TRY._serialized_end=476 _CLASS_FOR._serialized_start=478 _CLASS_FOR._serialized_end=506 _EXTENDCLASS._serialized_start=516 _EXTENDCLASS._serialized_end=579 _TESTFULLKEYWORD._serialized_start=581 _TESTFULLKEYWORD._serialized_end=707 _LOTSNESTEDMESSAGE._serialized_start=710 _LOTSNESTEDMESSAGE._serialized_end=2667 _LOTSNESTEDMESSAGE_B0._serialized_start=731 _LOTSNESTEDMESSAGE_B0._serialized_end=735 _LOTSNESTEDMESSAGE_B1._serialized_start=737 _LOTSNESTEDMESSAGE_B1._serialized_end=741 _LOTSNESTEDMESSAGE_B2._serialized_start=743 _LOTSNESTEDMESSAGE_B2._serialized_end=747 _LOTSNESTEDMESSAGE_B3._serialized_start=749 _LOTSNESTEDMESSAGE_B3._serialized_end=753 _LOTSNESTEDMESSAGE_B4._serialized_start=755 _LOTSNESTEDMESSAGE_B4._serialized_end=759 _LOTSNESTEDMESSAGE_B5._serialized_start=761 _LOTSNESTEDMESSAGE_B5._serialized_end=765 _LOTSNESTEDMESSAGE_B6._serialized_start=767 _LOTSNESTEDMESSAGE_B6._serialized_end=771 _LOTSNESTEDMESSAGE_B7._serialized_start=773 _LOTSNESTEDMESSAGE_B7._serialized_end=777 _LOTSNESTEDMESSAGE_B8._serialized_start=779 _LOTSNESTEDMESSAGE_B8._serialized_end=783 _LOTSNESTEDMESSAGE_B9._serialized_start=785 _LOTSNESTEDMESSAGE_B9._serialized_end=789 _LOTSNESTEDMESSAGE_B10._serialized_start=791 _LOTSNESTEDMESSAGE_B10._serialized_end=796 _LOTSNESTEDMESSAGE_B11._serialized_start=798 _LOTSNESTEDMESSAGE_B11._serialized_end=803 _LOTSNESTEDMESSAGE_B12._serialized_start=805 _LOTSNESTEDMESSAGE_B12._serialized_end=810 _LOTSNESTEDMESSAGE_B13._serialized_start=812 _LOTSNESTEDMESSAGE_B13._serialized_end=817 _LOTSNESTEDMESSAGE_B14._serialized_start=819 _LOTSNESTEDMESSAGE_B14._serialized_end=824 _LOTSNESTEDMESSAGE_B15._serialized_start=826 _LOTSNESTEDMESSAGE_B15._serialized_end=831 _LOTSNESTEDMESSAGE_B16._serialized_start=833 _LOTSNESTEDMESSAGE_B16._serialized_end=838 _LOTSNESTEDMESSAGE_B17._serialized_start=840 _LOTSNESTEDMESSAGE_B17._serialized_end=845 _LOTSNESTEDMESSAGE_B18._serialized_start=847 _LOTSNESTEDMESSAGE_B18._serialized_end=852 _LOTSNESTEDMESSAGE_B19._serialized_start=854 _LOTSNESTEDMESSAGE_B19._serialized_end=859 _LOTSNESTEDMESSAGE_B20._serialized_start=861 _LOTSNESTEDMESSAGE_B20._serialized_end=866 _LOTSNESTEDMESSAGE_B21._serialized_start=868 _LOTSNESTEDMESSAGE_B21._serialized_end=873 _LOTSNESTEDMESSAGE_B22._serialized_start=875 _LOTSNESTEDMESSAGE_B22._serialized_end=880 _LOTSNESTEDMESSAGE_B23._serialized_start=882 _LOTSNESTEDMESSAGE_B23._serialized_end=887 _LOTSNESTEDMESSAGE_B24._serialized_start=889 _LOTSNESTEDMESSAGE_B24._serialized_end=894 _LOTSNESTEDMESSAGE_B25._serialized_start=896 _LOTSNESTEDMESSAGE_B25._serialized_end=901 _LOTSNESTEDMESSAGE_B26._serialized_start=903 _LOTSNESTEDMESSAGE_B26._serialized_end=908 _LOTSNESTEDMESSAGE_B27._serialized_start=910 _LOTSNESTEDMESSAGE_B27._serialized_end=915 _LOTSNESTEDMESSAGE_B28._serialized_start=917 _LOTSNESTEDMESSAGE_B28._serialized_end=922 _LOTSNESTEDMESSAGE_B29._serialized_start=924 _LOTSNESTEDMESSAGE_B29._serialized_end=929 _LOTSNESTEDMESSAGE_B30._serialized_start=931 _LOTSNESTEDMESSAGE_B30._serialized_end=936 _LOTSNESTEDMESSAGE_B31._serialized_start=938 _LOTSNESTEDMESSAGE_B31._serialized_end=943 _LOTSNESTEDMESSAGE_B32._serialized_start=945 _LOTSNESTEDMESSAGE_B32._serialized_end=950 _LOTSNESTEDMESSAGE_B33._serialized_start=952 _LOTSNESTEDMESSAGE_B33._serialized_end=957 _LOTSNESTEDMESSAGE_B34._serialized_start=959 _LOTSNESTEDMESSAGE_B34._serialized_end=964 _LOTSNESTEDMESSAGE_B35._serialized_start=966 _LOTSNESTEDMESSAGE_B35._serialized_end=971 _LOTSNESTEDMESSAGE_B36._serialized_start=973 _LOTSNESTEDMESSAGE_B36._serialized_end=978 _LOTSNESTEDMESSAGE_B37._serialized_start=980 _LOTSNESTEDMESSAGE_B37._serialized_end=985 _LOTSNESTEDMESSAGE_B38._serialized_start=987 _LOTSNESTEDMESSAGE_B38._serialized_end=992 _LOTSNESTEDMESSAGE_B39._serialized_start=994 _LOTSNESTEDMESSAGE_B39._serialized_end=999 _LOTSNESTEDMESSAGE_B40._serialized_start=1001 _LOTSNESTEDMESSAGE_B40._serialized_end=1006 _LOTSNESTEDMESSAGE_B41._serialized_start=1008 _LOTSNESTEDMESSAGE_B41._serialized_end=1013 _LOTSNESTEDMESSAGE_B42._serialized_start=1015 _LOTSNESTEDMESSAGE_B42._serialized_end=1020 _LOTSNESTEDMESSAGE_B43._serialized_start=1022 _LOTSNESTEDMESSAGE_B43._serialized_end=1027 _LOTSNESTEDMESSAGE_B44._serialized_start=1029 _LOTSNESTEDMESSAGE_B44._serialized_end=1034 _LOTSNESTEDMESSAGE_B45._serialized_start=1036 _LOTSNESTEDMESSAGE_B45._serialized_end=1041 _LOTSNESTEDMESSAGE_B46._serialized_start=1043 _LOTSNESTEDMESSAGE_B46._serialized_end=1048 _LOTSNESTEDMESSAGE_B47._serialized_start=1050 _LOTSNESTEDMESSAGE_B47._serialized_end=1055 _LOTSNESTEDMESSAGE_B48._serialized_start=1057 _LOTSNESTEDMESSAGE_B48._serialized_end=1062 _LOTSNESTEDMESSAGE_B49._serialized_start=1064 _LOTSNESTEDMESSAGE_B49._serialized_end=1069 _LOTSNESTEDMESSAGE_B50._serialized_start=1071 _LOTSNESTEDMESSAGE_B50._serialized_end=1076 _LOTSNESTEDMESSAGE_B51._serialized_start=1078 _LOTSNESTEDMESSAGE_B51._serialized_end=1083 _LOTSNESTEDMESSAGE_B52._serialized_start=1085 _LOTSNESTEDMESSAGE_B52._serialized_end=1090 _LOTSNESTEDMESSAGE_B53._serialized_start=1092 _LOTSNESTEDMESSAGE_B53._serialized_end=1097 _LOTSNESTEDMESSAGE_B54._serialized_start=1099 _LOTSNESTEDMESSAGE_B54._serialized_end=1104 _LOTSNESTEDMESSAGE_B55._serialized_start=1106 _LOTSNESTEDMESSAGE_B55._serialized_end=1111 _LOTSNESTEDMESSAGE_B56._serialized_start=1113 _LOTSNESTEDMESSAGE_B56._serialized_end=1118 _LOTSNESTEDMESSAGE_B57._serialized_start=1120 _LOTSNESTEDMESSAGE_B57._serialized_end=1125 _LOTSNESTEDMESSAGE_B58._serialized_start=1127 _LOTSNESTEDMESSAGE_B58._serialized_end=1132 _LOTSNESTEDMESSAGE_B59._serialized_start=1134 _LOTSNESTEDMESSAGE_B59._serialized_end=1139 _LOTSNESTEDMESSAGE_B60._serialized_start=1141 _LOTSNESTEDMESSAGE_B60._serialized_end=1146 _LOTSNESTEDMESSAGE_B61._serialized_start=1148 _LOTSNESTEDMESSAGE_B61._serialized_end=1153 _LOTSNESTEDMESSAGE_B62._serialized_start=1155 _LOTSNESTEDMESSAGE_B62._serialized_end=1160 _LOTSNESTEDMESSAGE_B63._serialized_start=1162 _LOTSNESTEDMESSAGE_B63._serialized_end=1167 _LOTSNESTEDMESSAGE_B64._serialized_start=1169 _LOTSNESTEDMESSAGE_B64._serialized_end=1174 _LOTSNESTEDMESSAGE_B65._serialized_start=1176 _LOTSNESTEDMESSAGE_B65._serialized_end=1181 _LOTSNESTEDMESSAGE_B66._serialized_start=1183 _LOTSNESTEDMESSAGE_B66._serialized_end=1188 _LOTSNESTEDMESSAGE_B67._serialized_start=1190 _LOTSNESTEDMESSAGE_B67._serialized_end=1195 _LOTSNESTEDMESSAGE_B68._serialized_start=1197 _LOTSNESTEDMESSAGE_B68._serialized_end=1202 _LOTSNESTEDMESSAGE_B69._serialized_start=1204 _LOTSNESTEDMESSAGE_B69._serialized_end=1209 _LOTSNESTEDMESSAGE_B70._serialized_start=1211 _LOTSNESTEDMESSAGE_B70._serialized_end=1216 _LOTSNESTEDMESSAGE_B71._serialized_start=1218 _LOTSNESTEDMESSAGE_B71._serialized_end=1223 _LOTSNESTEDMESSAGE_B72._serialized_start=1225 _LOTSNESTEDMESSAGE_B72._serialized_end=1230 _LOTSNESTEDMESSAGE_B73._serialized_start=1232 _LOTSNESTEDMESSAGE_B73._serialized_end=1237 _LOTSNESTEDMESSAGE_B74._serialized_start=1239 _LOTSNESTEDMESSAGE_B74._serialized_end=1244 _LOTSNESTEDMESSAGE_B75._serialized_start=1246 _LOTSNESTEDMESSAGE_B75._serialized_end=1251 _LOTSNESTEDMESSAGE_B76._serialized_start=1253 _LOTSNESTEDMESSAGE_B76._serialized_end=1258 _LOTSNESTEDMESSAGE_B77._serialized_start=1260 _LOTSNESTEDMESSAGE_B77._serialized_end=1265 _LOTSNESTEDMESSAGE_B78._serialized_start=1267 _LOTSNESTEDMESSAGE_B78._serialized_end=1272 _LOTSNESTEDMESSAGE_B79._serialized_start=1274 _LOTSNESTEDMESSAGE_B79._serialized_end=1279 _LOTSNESTEDMESSAGE_B80._serialized_start=1281 _LOTSNESTEDMESSAGE_B80._serialized_end=1286 _LOTSNESTEDMESSAGE_B81._serialized_start=1288 _LOTSNESTEDMESSAGE_B81._serialized_end=1293 _LOTSNESTEDMESSAGE_B82._serialized_start=1295 _LOTSNESTEDMESSAGE_B82._serialized_end=1300 _LOTSNESTEDMESSAGE_B83._serialized_start=1302 _LOTSNESTEDMESSAGE_B83._serialized_end=1307 _LOTSNESTEDMESSAGE_B84._serialized_start=1309 _LOTSNESTEDMESSAGE_B84._serialized_end=1314 _LOTSNESTEDMESSAGE_B85._serialized_start=1316 _LOTSNESTEDMESSAGE_B85._serialized_end=1321 _LOTSNESTEDMESSAGE_B86._serialized_start=1323 _LOTSNESTEDMESSAGE_B86._serialized_end=1328 _LOTSNESTEDMESSAGE_B87._serialized_start=1330 _LOTSNESTEDMESSAGE_B87._serialized_end=1335 _LOTSNESTEDMESSAGE_B88._serialized_start=1337 _LOTSNESTEDMESSAGE_B88._serialized_end=1342 _LOTSNESTEDMESSAGE_B89._serialized_start=1344 _LOTSNESTEDMESSAGE_B89._serialized_end=1349 _LOTSNESTEDMESSAGE_B90._serialized_start=1351 _LOTSNESTEDMESSAGE_B90._serialized_end=1356 _LOTSNESTEDMESSAGE_B91._serialized_start=1358 _LOTSNESTEDMESSAGE_B91._serialized_end=1363 _LOTSNESTEDMESSAGE_B92._serialized_start=1365 _LOTSNESTEDMESSAGE_B92._serialized_end=1370 _LOTSNESTEDMESSAGE_B93._serialized_start=1372 _LOTSNESTEDMESSAGE_B93._serialized_end=1377 _LOTSNESTEDMESSAGE_B94._serialized_start=1379 _LOTSNESTEDMESSAGE_B94._serialized_end=1384 _LOTSNESTEDMESSAGE_B95._serialized_start=1386 _LOTSNESTEDMESSAGE_B95._serialized_end=1391 _LOTSNESTEDMESSAGE_B96._serialized_start=1393 _LOTSNESTEDMESSAGE_B96._serialized_end=1398 _LOTSNESTEDMESSAGE_B97._serialized_start=1400 _LOTSNESTEDMESSAGE_B97._serialized_end=1405 _LOTSNESTEDMESSAGE_B98._serialized_start=1407 _LOTSNESTEDMESSAGE_B98._serialized_end=1412 _LOTSNESTEDMESSAGE_B99._serialized_start=1414 _LOTSNESTEDMESSAGE_B99._serialized_end=1419 _LOTSNESTEDMESSAGE_B100._serialized_start=1421 _LOTSNESTEDMESSAGE_B100._serialized_end=1427 _LOTSNESTEDMESSAGE_B101._serialized_start=1429 _LOTSNESTEDMESSAGE_B101._serialized_end=1435 _LOTSNESTEDMESSAGE_B102._serialized_start=1437 _LOTSNESTEDMESSAGE_B102._serialized_end=1443 _LOTSNESTEDMESSAGE_B103._serialized_start=1445 _LOTSNESTEDMESSAGE_B103._serialized_end=1451 _LOTSNESTEDMESSAGE_B104._serialized_start=1453 _LOTSNESTEDMESSAGE_B104._serialized_end=1459 _LOTSNESTEDMESSAGE_B105._serialized_start=1461 _LOTSNESTEDMESSAGE_B105._serialized_end=1467 _LOTSNESTEDMESSAGE_B106._serialized_start=1469 _LOTSNESTEDMESSAGE_B106._serialized_end=1475 _LOTSNESTEDMESSAGE_B107._serialized_start=1477 _LOTSNESTEDMESSAGE_B107._serialized_end=1483 _LOTSNESTEDMESSAGE_B108._serialized_start=1485 _LOTSNESTEDMESSAGE_B108._serialized_end=1491 _LOTSNESTEDMESSAGE_B109._serialized_start=1493 _LOTSNESTEDMESSAGE_B109._serialized_end=1499 _LOTSNESTEDMESSAGE_B110._serialized_start=1501 _LOTSNESTEDMESSAGE_B110._serialized_end=1507 _LOTSNESTEDMESSAGE_B111._serialized_start=1509 _LOTSNESTEDMESSAGE_B111._serialized_end=1515 _LOTSNESTEDMESSAGE_B112._serialized_start=1517 _LOTSNESTEDMESSAGE_B112._serialized_end=1523 _LOTSNESTEDMESSAGE_B113._serialized_start=1525 _LOTSNESTEDMESSAGE_B113._serialized_end=1531 _LOTSNESTEDMESSAGE_B114._serialized_start=1533 _LOTSNESTEDMESSAGE_B114._serialized_end=1539 _LOTSNESTEDMESSAGE_B115._serialized_start=1541 _LOTSNESTEDMESSAGE_B115._serialized_end=1547 _LOTSNESTEDMESSAGE_B116._serialized_start=1549 _LOTSNESTEDMESSAGE_B116._serialized_end=1555 _LOTSNESTEDMESSAGE_B117._serialized_start=1557 _LOTSNESTEDMESSAGE_B117._serialized_end=1563 _LOTSNESTEDMESSAGE_B118._serialized_start=1565 _LOTSNESTEDMESSAGE_B118._serialized_end=1571 _LOTSNESTEDMESSAGE_B119._serialized_start=1573 _LOTSNESTEDMESSAGE_B119._serialized_end=1579 _LOTSNESTEDMESSAGE_B120._serialized_start=1581 _LOTSNESTEDMESSAGE_B120._serialized_end=1587 _LOTSNESTEDMESSAGE_B121._serialized_start=1589 _LOTSNESTEDMESSAGE_B121._serialized_end=1595 _LOTSNESTEDMESSAGE_B122._serialized_start=1597 _LOTSNESTEDMESSAGE_B122._serialized_end=1603 _LOTSNESTEDMESSAGE_B123._serialized_start=1605 _LOTSNESTEDMESSAGE_B123._serialized_end=1611 _LOTSNESTEDMESSAGE_B124._serialized_start=1613 _LOTSNESTEDMESSAGE_B124._serialized_end=1619 _LOTSNESTEDMESSAGE_B125._serialized_start=1621 _LOTSNESTEDMESSAGE_B125._serialized_end=1627 _LOTSNESTEDMESSAGE_B126._serialized_start=1629 _LOTSNESTEDMESSAGE_B126._serialized_end=1635 _LOTSNESTEDMESSAGE_B127._serialized_start=1637 _LOTSNESTEDMESSAGE_B127._serialized_end=1643 _LOTSNESTEDMESSAGE_B128._serialized_start=1645 _LOTSNESTEDMESSAGE_B128._serialized_end=1651 _LOTSNESTEDMESSAGE_B129._serialized_start=1653 _LOTSNESTEDMESSAGE_B129._serialized_end=1659 _LOTSNESTEDMESSAGE_B130._serialized_start=1661 _LOTSNESTEDMESSAGE_B130._serialized_end=1667 _LOTSNESTEDMESSAGE_B131._serialized_start=1669 _LOTSNESTEDMESSAGE_B131._serialized_end=1675 _LOTSNESTEDMESSAGE_B132._serialized_start=1677 _LOTSNESTEDMESSAGE_B132._serialized_end=1683 _LOTSNESTEDMESSAGE_B133._serialized_start=1685 _LOTSNESTEDMESSAGE_B133._serialized_end=1691 _LOTSNESTEDMESSAGE_B134._serialized_start=1693 _LOTSNESTEDMESSAGE_B134._serialized_end=1699 _LOTSNESTEDMESSAGE_B135._serialized_start=1701 _LOTSNESTEDMESSAGE_B135._serialized_end=1707 _LOTSNESTEDMESSAGE_B136._serialized_start=1709 _LOTSNESTEDMESSAGE_B136._serialized_end=1715 _LOTSNESTEDMESSAGE_B137._serialized_start=1717 _LOTSNESTEDMESSAGE_B137._serialized_end=1723 _LOTSNESTEDMESSAGE_B138._serialized_start=1725 _LOTSNESTEDMESSAGE_B138._serialized_end=1731 _LOTSNESTEDMESSAGE_B139._serialized_start=1733 _LOTSNESTEDMESSAGE_B139._serialized_end=1739 _LOTSNESTEDMESSAGE_B140._serialized_start=1741 _LOTSNESTEDMESSAGE_B140._serialized_end=1747 _LOTSNESTEDMESSAGE_B141._serialized_start=1749 _LOTSNESTEDMESSAGE_B141._serialized_end=1755 _LOTSNESTEDMESSAGE_B142._serialized_start=1757 _LOTSNESTEDMESSAGE_B142._serialized_end=1763 _LOTSNESTEDMESSAGE_B143._serialized_start=1765 _LOTSNESTEDMESSAGE_B143._serialized_end=1771 _LOTSNESTEDMESSAGE_B144._serialized_start=1773 _LOTSNESTEDMESSAGE_B144._serialized_end=1779 _LOTSNESTEDMESSAGE_B145._serialized_start=1781 _LOTSNESTEDMESSAGE_B145._serialized_end=1787 _LOTSNESTEDMESSAGE_B146._serialized_start=1789 _LOTSNESTEDMESSAGE_B146._serialized_end=1795 _LOTSNESTEDMESSAGE_B147._serialized_start=1797 _LOTSNESTEDMESSAGE_B147._serialized_end=1803 _LOTSNESTEDMESSAGE_B148._serialized_start=1805 _LOTSNESTEDMESSAGE_B148._serialized_end=1811 _LOTSNESTEDMESSAGE_B149._serialized_start=1813 _LOTSNESTEDMESSAGE_B149._serialized_end=1819 _LOTSNESTEDMESSAGE_B150._serialized_start=1821 _LOTSNESTEDMESSAGE_B150._serialized_end=1827 _LOTSNESTEDMESSAGE_B151._serialized_start=1829 _LOTSNESTEDMESSAGE_B151._serialized_end=1835 _LOTSNESTEDMESSAGE_B152._serialized_start=1837 _LOTSNESTEDMESSAGE_B152._serialized_end=1843 _LOTSNESTEDMESSAGE_B153._serialized_start=1845 _LOTSNESTEDMESSAGE_B153._serialized_end=1851 _LOTSNESTEDMESSAGE_B154._serialized_start=1853 _LOTSNESTEDMESSAGE_B154._serialized_end=1859 _LOTSNESTEDMESSAGE_B155._serialized_start=1861 _LOTSNESTEDMESSAGE_B155._serialized_end=1867 _LOTSNESTEDMESSAGE_B156._serialized_start=1869 _LOTSNESTEDMESSAGE_B156._serialized_end=1875 _LOTSNESTEDMESSAGE_B157._serialized_start=1877 _LOTSNESTEDMESSAGE_B157._serialized_end=1883 _LOTSNESTEDMESSAGE_B158._serialized_start=1885 _LOTSNESTEDMESSAGE_B158._serialized_end=1891 _LOTSNESTEDMESSAGE_B159._serialized_start=1893 _LOTSNESTEDMESSAGE_B159._serialized_end=1899 _LOTSNESTEDMESSAGE_B160._serialized_start=1901 _LOTSNESTEDMESSAGE_B160._serialized_end=1907 _LOTSNESTEDMESSAGE_B161._serialized_start=1909 _LOTSNESTEDMESSAGE_B161._serialized_end=1915 _LOTSNESTEDMESSAGE_B162._serialized_start=1917 _LOTSNESTEDMESSAGE_B162._serialized_end=1923 _LOTSNESTEDMESSAGE_B163._serialized_start=1925 _LOTSNESTEDMESSAGE_B163._serialized_end=1931 _LOTSNESTEDMESSAGE_B164._serialized_start=1933 _LOTSNESTEDMESSAGE_B164._serialized_end=1939 _LOTSNESTEDMESSAGE_B165._serialized_start=1941 _LOTSNESTEDMESSAGE_B165._serialized_end=1947 _LOTSNESTEDMESSAGE_B166._serialized_start=1949 _LOTSNESTEDMESSAGE_B166._serialized_end=1955 _LOTSNESTEDMESSAGE_B167._serialized_start=1957 _LOTSNESTEDMESSAGE_B167._serialized_end=1963 _LOTSNESTEDMESSAGE_B168._serialized_start=1965 _LOTSNESTEDMESSAGE_B168._serialized_end=1971 _LOTSNESTEDMESSAGE_B169._serialized_start=1973 _LOTSNESTEDMESSAGE_B169._serialized_end=1979 _LOTSNESTEDMESSAGE_B170._serialized_start=1981 _LOTSNESTEDMESSAGE_B170._serialized_end=1987 _LOTSNESTEDMESSAGE_B171._serialized_start=1989 _LOTSNESTEDMESSAGE_B171._serialized_end=1995 _LOTSNESTEDMESSAGE_B172._serialized_start=1997 _LOTSNESTEDMESSAGE_B172._serialized_end=2003 _LOTSNESTEDMESSAGE_B173._serialized_start=2005 _LOTSNESTEDMESSAGE_B173._serialized_end=2011 _LOTSNESTEDMESSAGE_B174._serialized_start=2013 _LOTSNESTEDMESSAGE_B174._serialized_end=2019 _LOTSNESTEDMESSAGE_B175._serialized_start=2021 _LOTSNESTEDMESSAGE_B175._serialized_end=2027 _LOTSNESTEDMESSAGE_B176._serialized_start=2029 _LOTSNESTEDMESSAGE_B176._serialized_end=2035 _LOTSNESTEDMESSAGE_B177._serialized_start=2037 _LOTSNESTEDMESSAGE_B177._serialized_end=2043 _LOTSNESTEDMESSAGE_B178._serialized_start=2045 _LOTSNESTEDMESSAGE_B178._serialized_end=2051 _LOTSNESTEDMESSAGE_B179._serialized_start=2053 _LOTSNESTEDMESSAGE_B179._serialized_end=2059 _LOTSNESTEDMESSAGE_B180._serialized_start=2061 _LOTSNESTEDMESSAGE_B180._serialized_end=2067 _LOTSNESTEDMESSAGE_B181._serialized_start=2069 _LOTSNESTEDMESSAGE_B181._serialized_end=2075 _LOTSNESTEDMESSAGE_B182._serialized_start=2077 _LOTSNESTEDMESSAGE_B182._serialized_end=2083 _LOTSNESTEDMESSAGE_B183._serialized_start=2085 _LOTSNESTEDMESSAGE_B183._serialized_end=2091 _LOTSNESTEDMESSAGE_B184._serialized_start=2093 _LOTSNESTEDMESSAGE_B184._serialized_end=2099 _LOTSNESTEDMESSAGE_B185._serialized_start=2101 _LOTSNESTEDMESSAGE_B185._serialized_end=2107 _LOTSNESTEDMESSAGE_B186._serialized_start=2109 _LOTSNESTEDMESSAGE_B186._serialized_end=2115 _LOTSNESTEDMESSAGE_B187._serialized_start=2117 _LOTSNESTEDMESSAGE_B187._serialized_end=2123 _LOTSNESTEDMESSAGE_B188._serialized_start=2125 _LOTSNESTEDMESSAGE_B188._serialized_end=2131 _LOTSNESTEDMESSAGE_B189._serialized_start=2133 _LOTSNESTEDMESSAGE_B189._serialized_end=2139 _LOTSNESTEDMESSAGE_B190._serialized_start=2141 _LOTSNESTEDMESSAGE_B190._serialized_end=2147 _LOTSNESTEDMESSAGE_B191._serialized_start=2149 _LOTSNESTEDMESSAGE_B191._serialized_end=2155 _LOTSNESTEDMESSAGE_B192._serialized_start=2157 _LOTSNESTEDMESSAGE_B192._serialized_end=2163 _LOTSNESTEDMESSAGE_B193._serialized_start=2165 _LOTSNESTEDMESSAGE_B193._serialized_end=2171 _LOTSNESTEDMESSAGE_B194._serialized_start=2173 _LOTSNESTEDMESSAGE_B194._serialized_end=2179 _LOTSNESTEDMESSAGE_B195._serialized_start=2181 _LOTSNESTEDMESSAGE_B195._serialized_end=2187 _LOTSNESTEDMESSAGE_B196._serialized_start=2189 _LOTSNESTEDMESSAGE_B196._serialized_end=2195 _LOTSNESTEDMESSAGE_B197._serialized_start=2197 _LOTSNESTEDMESSAGE_B197._serialized_end=2203 _LOTSNESTEDMESSAGE_B198._serialized_start=2205 _LOTSNESTEDMESSAGE_B198._serialized_end=2211 _LOTSNESTEDMESSAGE_B199._serialized_start=2213 _LOTSNESTEDMESSAGE_B199._serialized_end=2219 _LOTSNESTEDMESSAGE_B200._serialized_start=2221 _LOTSNESTEDMESSAGE_B200._serialized_end=2227 _LOTSNESTEDMESSAGE_B201._serialized_start=2229 _LOTSNESTEDMESSAGE_B201._serialized_end=2235 _LOTSNESTEDMESSAGE_B202._serialized_start=2237 _LOTSNESTEDMESSAGE_B202._serialized_end=2243 _LOTSNESTEDMESSAGE_B203._serialized_start=2245 _LOTSNESTEDMESSAGE_B203._serialized_end=2251 _LOTSNESTEDMESSAGE_B204._serialized_start=2253 _LOTSNESTEDMESSAGE_B204._serialized_end=2259 _LOTSNESTEDMESSAGE_B205._serialized_start=2261 _LOTSNESTEDMESSAGE_B205._serialized_end=2267 _LOTSNESTEDMESSAGE_B206._serialized_start=2269 _LOTSNESTEDMESSAGE_B206._serialized_end=2275 _LOTSNESTEDMESSAGE_B207._serialized_start=2277 _LOTSNESTEDMESSAGE_B207._serialized_end=2283 _LOTSNESTEDMESSAGE_B208._serialized_start=2285 _LOTSNESTEDMESSAGE_B208._serialized_end=2291 _LOTSNESTEDMESSAGE_B209._serialized_start=2293 _LOTSNESTEDMESSAGE_B209._serialized_end=2299 _LOTSNESTEDMESSAGE_B210._serialized_start=2301 _LOTSNESTEDMESSAGE_B210._serialized_end=2307 _LOTSNESTEDMESSAGE_B211._serialized_start=2309 _LOTSNESTEDMESSAGE_B211._serialized_end=2315 _LOTSNESTEDMESSAGE_B212._serialized_start=2317 _LOTSNESTEDMESSAGE_B212._serialized_end=2323 _LOTSNESTEDMESSAGE_B213._serialized_start=2325 _LOTSNESTEDMESSAGE_B213._serialized_end=2331 _LOTSNESTEDMESSAGE_B214._serialized_start=2333 _LOTSNESTEDMESSAGE_B214._serialized_end=2339 _LOTSNESTEDMESSAGE_B215._serialized_start=2341 _LOTSNESTEDMESSAGE_B215._serialized_end=2347 _LOTSNESTEDMESSAGE_B216._serialized_start=2349 _LOTSNESTEDMESSAGE_B216._serialized_end=2355 _LOTSNESTEDMESSAGE_B217._serialized_start=2357 _LOTSNESTEDMESSAGE_B217._serialized_end=2363 _LOTSNESTEDMESSAGE_B218._serialized_start=2365 _LOTSNESTEDMESSAGE_B218._serialized_end=2371 _LOTSNESTEDMESSAGE_B219._serialized_start=2373 _LOTSNESTEDMESSAGE_B219._serialized_end=2379 _LOTSNESTEDMESSAGE_B220._serialized_start=2381 _LOTSNESTEDMESSAGE_B220._serialized_end=2387 _LOTSNESTEDMESSAGE_B221._serialized_start=2389 _LOTSNESTEDMESSAGE_B221._serialized_end=2395 _LOTSNESTEDMESSAGE_B222._serialized_start=2397 _LOTSNESTEDMESSAGE_B222._serialized_end=2403 _LOTSNESTEDMESSAGE_B223._serialized_start=2405 _LOTSNESTEDMESSAGE_B223._serialized_end=2411 _LOTSNESTEDMESSAGE_B224._serialized_start=2413 _LOTSNESTEDMESSAGE_B224._serialized_end=2419 _LOTSNESTEDMESSAGE_B225._serialized_start=2421 _LOTSNESTEDMESSAGE_B225._serialized_end=2427 _LOTSNESTEDMESSAGE_B226._serialized_start=2429 _LOTSNESTEDMESSAGE_B226._serialized_end=2435 _LOTSNESTEDMESSAGE_B227._serialized_start=2437 _LOTSNESTEDMESSAGE_B227._serialized_end=2443 _LOTSNESTEDMESSAGE_B228._serialized_start=2445 _LOTSNESTEDMESSAGE_B228._serialized_end=2451 _LOTSNESTEDMESSAGE_B229._serialized_start=2453 _LOTSNESTEDMESSAGE_B229._serialized_end=2459 _LOTSNESTEDMESSAGE_B230._serialized_start=2461 _LOTSNESTEDMESSAGE_B230._serialized_end=2467 _LOTSNESTEDMESSAGE_B231._serialized_start=2469 _LOTSNESTEDMESSAGE_B231._serialized_end=2475 _LOTSNESTEDMESSAGE_B232._serialized_start=2477 _LOTSNESTEDMESSAGE_B232._serialized_end=2483 _LOTSNESTEDMESSAGE_B233._serialized_start=2485 _LOTSNESTEDMESSAGE_B233._serialized_end=2491 _LOTSNESTEDMESSAGE_B234._serialized_start=2493 _LOTSNESTEDMESSAGE_B234._serialized_end=2499 _LOTSNESTEDMESSAGE_B235._serialized_start=2501 _LOTSNESTEDMESSAGE_B235._serialized_end=2507 _LOTSNESTEDMESSAGE_B236._serialized_start=2509 _LOTSNESTEDMESSAGE_B236._serialized_end=2515 _LOTSNESTEDMESSAGE_B237._serialized_start=2517 _LOTSNESTEDMESSAGE_B237._serialized_end=2523 _LOTSNESTEDMESSAGE_B238._serialized_start=2525 _LOTSNESTEDMESSAGE_B238._serialized_end=2531 _LOTSNESTEDMESSAGE_B239._serialized_start=2533 _LOTSNESTEDMESSAGE_B239._serialized_end=2539 _LOTSNESTEDMESSAGE_B240._serialized_start=2541 _LOTSNESTEDMESSAGE_B240._serialized_end=2547 _LOTSNESTEDMESSAGE_B241._serialized_start=2549 _LOTSNESTEDMESSAGE_B241._serialized_end=2555 _LOTSNESTEDMESSAGE_B242._serialized_start=2557 _LOTSNESTEDMESSAGE_B242._serialized_end=2563 _LOTSNESTEDMESSAGE_B243._serialized_start=2565 _LOTSNESTEDMESSAGE_B243._serialized_end=2571 _LOTSNESTEDMESSAGE_B244._serialized_start=2573 _LOTSNESTEDMESSAGE_B244._serialized_end=2579 _LOTSNESTEDMESSAGE_B245._serialized_start=2581 _LOTSNESTEDMESSAGE_B245._serialized_end=2587 _LOTSNESTEDMESSAGE_B246._serialized_start=2589 _LOTSNESTEDMESSAGE_B246._serialized_end=2595 _LOTSNESTEDMESSAGE_B247._serialized_start=2597 _LOTSNESTEDMESSAGE_B247._serialized_end=2603 _LOTSNESTEDMESSAGE_B248._serialized_start=2605 _LOTSNESTEDMESSAGE_B248._serialized_end=2611 _LOTSNESTEDMESSAGE_B249._serialized_start=2613 _LOTSNESTEDMESSAGE_B249._serialized_end=2619 _LOTSNESTEDMESSAGE_B250._serialized_start=2621 _LOTSNESTEDMESSAGE_B250._serialized_end=2627 _LOTSNESTEDMESSAGE_B251._serialized_start=2629 _LOTSNESTEDMESSAGE_B251._serialized_end=2635 _LOTSNESTEDMESSAGE_B252._serialized_start=2637 _LOTSNESTEDMESSAGE_B252._serialized_end=2643 _LOTSNESTEDMESSAGE_B253._serialized_start=2645 _LOTSNESTEDMESSAGE_B253._serialized_end=2651 _LOTSNESTEDMESSAGE_B254._serialized_start=2653 _LOTSNESTEDMESSAGE_B254._serialized_end=2659 _LOTSNESTEDMESSAGE_B255._serialized_start=2661 _LOTSNESTEDMESSAGE_B255._serialized_end=2667 # @@protoc_insertion_point(module_scope) ================================================ FILE: openpype/hosts/hiero/vendor/google/protobuf/internal/no_package_pb2.py ================================================ # -*- coding: utf-8 -*- # Generated by the protocol buffer compiler. DO NOT EDIT! # source: google/protobuf/internal/no_package.proto """Generated protocol buffer code.""" from google.protobuf.internal import builder as _builder from google.protobuf import descriptor as _descriptor from google.protobuf import descriptor_pool as _descriptor_pool from google.protobuf import symbol_database as _symbol_database # @@protoc_insertion_point(imports) _sym_db = _symbol_database.Default() DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n)google/protobuf/internal/no_package.proto\";\n\x10NoPackageMessage\x12\'\n\x0fno_package_enum\x18\x01 \x01(\x0e\x32\x0e.NoPackageEnum*?\n\rNoPackageEnum\x12\x16\n\x12NO_PACKAGE_VALUE_0\x10\x00\x12\x16\n\x12NO_PACKAGE_VALUE_1\x10\x01') _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals()) _builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'google.protobuf.internal.no_package_pb2', globals()) if _descriptor._USE_C_DESCRIPTORS == False: DESCRIPTOR._options = None _NOPACKAGEENUM._serialized_start=106 _NOPACKAGEENUM._serialized_end=169 _NOPACKAGEMESSAGE._serialized_start=45 _NOPACKAGEMESSAGE._serialized_end=104 # @@protoc_insertion_point(module_scope) ================================================ FILE: openpype/hosts/hiero/vendor/google/protobuf/internal/python_message.py ================================================ # Protocol Buffers - Google's data interchange format # Copyright 2008 Google Inc. All rights reserved. # https://developers.google.com/protocol-buffers/ # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are # met: # # * Redistributions of source code must retain the above copyright # notice, this list of conditions and the following disclaimer. # * Redistributions in binary form must reproduce the above # copyright notice, this list of conditions and the following disclaimer # in the documentation and/or other materials provided with the # distribution. # * Neither the name of Google Inc. nor the names of its # contributors may be used to endorse or promote products derived from # this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. # This code is meant to work on Python 2.4 and above only. # # TODO(robinson): Helpers for verbose, common checks like seeing if a # descriptor's cpp_type is CPPTYPE_MESSAGE. """Contains a metaclass and helper functions used to create protocol message classes from Descriptor objects at runtime. Recall that a metaclass is the "type" of a class. (A class is to a metaclass what an instance is to a class.) In this case, we use the GeneratedProtocolMessageType metaclass to inject all the useful functionality into the classes output by the protocol compiler at compile-time. The upshot of all this is that the real implementation details for ALL pure-Python protocol buffers are *here in this file*. """ __author__ = 'robinson@google.com (Will Robinson)' from io import BytesIO import struct import sys import weakref # We use "as" to avoid name collisions with variables. from google.protobuf.internal import api_implementation from google.protobuf.internal import containers from google.protobuf.internal import decoder from google.protobuf.internal import encoder from google.protobuf.internal import enum_type_wrapper from google.protobuf.internal import extension_dict from google.protobuf.internal import message_listener as message_listener_mod from google.protobuf.internal import type_checkers from google.protobuf.internal import well_known_types from google.protobuf.internal import wire_format from google.protobuf import descriptor as descriptor_mod from google.protobuf import message as message_mod from google.protobuf import text_format _FieldDescriptor = descriptor_mod.FieldDescriptor _AnyFullTypeName = 'google.protobuf.Any' _ExtensionDict = extension_dict._ExtensionDict class GeneratedProtocolMessageType(type): """Metaclass for protocol message classes created at runtime from Descriptors. We add implementations for all methods described in the Message class. We also create properties to allow getting/setting all fields in the protocol message. Finally, we create slots to prevent users from accidentally "setting" nonexistent fields in the protocol message, which then wouldn't get serialized / deserialized properly. The protocol compiler currently uses this metaclass to create protocol message classes at runtime. Clients can also manually create their own classes at runtime, as in this example: mydescriptor = Descriptor(.....) factory = symbol_database.Default() factory.pool.AddDescriptor(mydescriptor) MyProtoClass = factory.GetPrototype(mydescriptor) myproto_instance = MyProtoClass() myproto.foo_field = 23 ... """ # Must be consistent with the protocol-compiler code in # proto2/compiler/internal/generator.*. _DESCRIPTOR_KEY = 'DESCRIPTOR' def __new__(cls, name, bases, dictionary): """Custom allocation for runtime-generated class types. We override __new__ because this is apparently the only place where we can meaningfully set __slots__ on the class we're creating(?). (The interplay between metaclasses and slots is not very well-documented). Args: name: Name of the class (ignored, but required by the metaclass protocol). bases: Base classes of the class we're constructing. (Should be message.Message). We ignore this field, but it's required by the metaclass protocol dictionary: The class dictionary of the class we're constructing. dictionary[_DESCRIPTOR_KEY] must contain a Descriptor object describing this protocol message type. Returns: Newly-allocated class. Raises: RuntimeError: Generated code only work with python cpp extension. """ descriptor = dictionary[GeneratedProtocolMessageType._DESCRIPTOR_KEY] if isinstance(descriptor, str): raise RuntimeError('The generated code only work with python cpp ' 'extension, but it is using pure python runtime.') # If a concrete class already exists for this descriptor, don't try to # create another. Doing so will break any messages that already exist with # the existing class. # # The C++ implementation appears to have its own internal `PyMessageFactory` # to achieve similar results. # # This most commonly happens in `text_format.py` when using descriptors from # a custom pool; it calls symbol_database.Global().getPrototype() on a # descriptor which already has an existing concrete class. new_class = getattr(descriptor, '_concrete_class', None) if new_class: return new_class if descriptor.full_name in well_known_types.WKTBASES: bases += (well_known_types.WKTBASES[descriptor.full_name],) _AddClassAttributesForNestedExtensions(descriptor, dictionary) _AddSlots(descriptor, dictionary) superclass = super(GeneratedProtocolMessageType, cls) new_class = superclass.__new__(cls, name, bases, dictionary) return new_class def __init__(cls, name, bases, dictionary): """Here we perform the majority of our work on the class. We add enum getters, an __init__ method, implementations of all Message methods, and properties for all fields in the protocol type. Args: name: Name of the class (ignored, but required by the metaclass protocol). bases: Base classes of the class we're constructing. (Should be message.Message). We ignore this field, but it's required by the metaclass protocol dictionary: The class dictionary of the class we're constructing. dictionary[_DESCRIPTOR_KEY] must contain a Descriptor object describing this protocol message type. """ descriptor = dictionary[GeneratedProtocolMessageType._DESCRIPTOR_KEY] # If this is an _existing_ class looked up via `_concrete_class` in the # __new__ method above, then we don't need to re-initialize anything. existing_class = getattr(descriptor, '_concrete_class', None) if existing_class: assert existing_class is cls, ( 'Duplicate `GeneratedProtocolMessageType` created for descriptor %r' % (descriptor.full_name)) return cls._decoders_by_tag = {} if (descriptor.has_options and descriptor.GetOptions().message_set_wire_format): cls._decoders_by_tag[decoder.MESSAGE_SET_ITEM_TAG] = ( decoder.MessageSetItemDecoder(descriptor), None) # Attach stuff to each FieldDescriptor for quick lookup later on. for field in descriptor.fields: _AttachFieldHelpers(cls, field) descriptor._concrete_class = cls # pylint: disable=protected-access _AddEnumValues(descriptor, cls) _AddInitMethod(descriptor, cls) _AddPropertiesForFields(descriptor, cls) _AddPropertiesForExtensions(descriptor, cls) _AddStaticMethods(cls) _AddMessageMethods(descriptor, cls) _AddPrivateHelperMethods(descriptor, cls) superclass = super(GeneratedProtocolMessageType, cls) superclass.__init__(name, bases, dictionary) # Stateless helpers for GeneratedProtocolMessageType below. # Outside clients should not access these directly. # # I opted not to make any of these methods on the metaclass, to make it more # clear that I'm not really using any state there and to keep clients from # thinking that they have direct access to these construction helpers. def _PropertyName(proto_field_name): """Returns the name of the public property attribute which clients can use to get and (in some cases) set the value of a protocol message field. Args: proto_field_name: The protocol message field name, exactly as it appears (or would appear) in a .proto file. """ # TODO(robinson): Escape Python keywords (e.g., yield), and test this support. # nnorwitz makes my day by writing: # """ # FYI. See the keyword module in the stdlib. This could be as simple as: # # if keyword.iskeyword(proto_field_name): # return proto_field_name + "_" # return proto_field_name # """ # Kenton says: The above is a BAD IDEA. People rely on being able to use # getattr() and setattr() to reflectively manipulate field values. If we # rename the properties, then every such user has to also make sure to apply # the same transformation. Note that currently if you name a field "yield", # you can still access it just fine using getattr/setattr -- it's not even # that cumbersome to do so. # TODO(kenton): Remove this method entirely if/when everyone agrees with my # position. return proto_field_name def _AddSlots(message_descriptor, dictionary): """Adds a __slots__ entry to dictionary, containing the names of all valid attributes for this message type. Args: message_descriptor: A Descriptor instance describing this message type. dictionary: Class dictionary to which we'll add a '__slots__' entry. """ dictionary['__slots__'] = ['_cached_byte_size', '_cached_byte_size_dirty', '_fields', '_unknown_fields', '_unknown_field_set', '_is_present_in_parent', '_listener', '_listener_for_children', '__weakref__', '_oneofs'] def _IsMessageSetExtension(field): return (field.is_extension and field.containing_type.has_options and field.containing_type.GetOptions().message_set_wire_format and field.type == _FieldDescriptor.TYPE_MESSAGE and field.label == _FieldDescriptor.LABEL_OPTIONAL) def _IsMapField(field): return (field.type == _FieldDescriptor.TYPE_MESSAGE and field.message_type.has_options and field.message_type.GetOptions().map_entry) def _IsMessageMapField(field): value_type = field.message_type.fields_by_name['value'] return value_type.cpp_type == _FieldDescriptor.CPPTYPE_MESSAGE def _AttachFieldHelpers(cls, field_descriptor): is_repeated = (field_descriptor.label == _FieldDescriptor.LABEL_REPEATED) is_packable = (is_repeated and wire_format.IsTypePackable(field_descriptor.type)) is_proto3 = field_descriptor.containing_type.syntax == 'proto3' if not is_packable: is_packed = False elif field_descriptor.containing_type.syntax == 'proto2': is_packed = (field_descriptor.has_options and field_descriptor.GetOptions().packed) else: has_packed_false = (field_descriptor.has_options and field_descriptor.GetOptions().HasField('packed') and field_descriptor.GetOptions().packed == False) is_packed = not has_packed_false is_map_entry = _IsMapField(field_descriptor) if is_map_entry: field_encoder = encoder.MapEncoder(field_descriptor) sizer = encoder.MapSizer(field_descriptor, _IsMessageMapField(field_descriptor)) elif _IsMessageSetExtension(field_descriptor): field_encoder = encoder.MessageSetItemEncoder(field_descriptor.number) sizer = encoder.MessageSetItemSizer(field_descriptor.number) else: field_encoder = type_checkers.TYPE_TO_ENCODER[field_descriptor.type]( field_descriptor.number, is_repeated, is_packed) sizer = type_checkers.TYPE_TO_SIZER[field_descriptor.type]( field_descriptor.number, is_repeated, is_packed) field_descriptor._encoder = field_encoder field_descriptor._sizer = sizer field_descriptor._default_constructor = _DefaultValueConstructorForField( field_descriptor) def AddDecoder(wiretype, is_packed): tag_bytes = encoder.TagBytes(field_descriptor.number, wiretype) decode_type = field_descriptor.type if (decode_type == _FieldDescriptor.TYPE_ENUM and type_checkers.SupportsOpenEnums(field_descriptor)): decode_type = _FieldDescriptor.TYPE_INT32 oneof_descriptor = None clear_if_default = False if field_descriptor.containing_oneof is not None: oneof_descriptor = field_descriptor elif (is_proto3 and not is_repeated and field_descriptor.cpp_type != _FieldDescriptor.CPPTYPE_MESSAGE): clear_if_default = True if is_map_entry: is_message_map = _IsMessageMapField(field_descriptor) field_decoder = decoder.MapDecoder( field_descriptor, _GetInitializeDefaultForMap(field_descriptor), is_message_map) elif decode_type == _FieldDescriptor.TYPE_STRING: field_decoder = decoder.StringDecoder( field_descriptor.number, is_repeated, is_packed, field_descriptor, field_descriptor._default_constructor, clear_if_default) elif field_descriptor.cpp_type == _FieldDescriptor.CPPTYPE_MESSAGE: field_decoder = type_checkers.TYPE_TO_DECODER[decode_type]( field_descriptor.number, is_repeated, is_packed, field_descriptor, field_descriptor._default_constructor) else: field_decoder = type_checkers.TYPE_TO_DECODER[decode_type]( field_descriptor.number, is_repeated, is_packed, # pylint: disable=protected-access field_descriptor, field_descriptor._default_constructor, clear_if_default) cls._decoders_by_tag[tag_bytes] = (field_decoder, oneof_descriptor) AddDecoder(type_checkers.FIELD_TYPE_TO_WIRE_TYPE[field_descriptor.type], False) if is_repeated and wire_format.IsTypePackable(field_descriptor.type): # To support wire compatibility of adding packed = true, add a decoder for # packed values regardless of the field's options. AddDecoder(wire_format.WIRETYPE_LENGTH_DELIMITED, True) def _AddClassAttributesForNestedExtensions(descriptor, dictionary): extensions = descriptor.extensions_by_name for extension_name, extension_field in extensions.items(): assert extension_name not in dictionary dictionary[extension_name] = extension_field def _AddEnumValues(descriptor, cls): """Sets class-level attributes for all enum fields defined in this message. Also exporting a class-level object that can name enum values. Args: descriptor: Descriptor object for this message type. cls: Class we're constructing for this message type. """ for enum_type in descriptor.enum_types: setattr(cls, enum_type.name, enum_type_wrapper.EnumTypeWrapper(enum_type)) for enum_value in enum_type.values: setattr(cls, enum_value.name, enum_value.number) def _GetInitializeDefaultForMap(field): if field.label != _FieldDescriptor.LABEL_REPEATED: raise ValueError('map_entry set on non-repeated field %s' % ( field.name)) fields_by_name = field.message_type.fields_by_name key_checker = type_checkers.GetTypeChecker(fields_by_name['key']) value_field = fields_by_name['value'] if _IsMessageMapField(field): def MakeMessageMapDefault(message): return containers.MessageMap( message._listener_for_children, value_field.message_type, key_checker, field.message_type) return MakeMessageMapDefault else: value_checker = type_checkers.GetTypeChecker(value_field) def MakePrimitiveMapDefault(message): return containers.ScalarMap( message._listener_for_children, key_checker, value_checker, field.message_type) return MakePrimitiveMapDefault def _DefaultValueConstructorForField(field): """Returns a function which returns a default value for a field. Args: field: FieldDescriptor object for this field. The returned function has one argument: message: Message instance containing this field, or a weakref proxy of same. That function in turn returns a default value for this field. The default value may refer back to |message| via a weak reference. """ if _IsMapField(field): return _GetInitializeDefaultForMap(field) if field.label == _FieldDescriptor.LABEL_REPEATED: if field.has_default_value and field.default_value != []: raise ValueError('Repeated field default value not empty list: %s' % ( field.default_value)) if field.cpp_type == _FieldDescriptor.CPPTYPE_MESSAGE: # We can't look at _concrete_class yet since it might not have # been set. (Depends on order in which we initialize the classes). message_type = field.message_type def MakeRepeatedMessageDefault(message): return containers.RepeatedCompositeFieldContainer( message._listener_for_children, field.message_type) return MakeRepeatedMessageDefault else: type_checker = type_checkers.GetTypeChecker(field) def MakeRepeatedScalarDefault(message): return containers.RepeatedScalarFieldContainer( message._listener_for_children, type_checker) return MakeRepeatedScalarDefault if field.cpp_type == _FieldDescriptor.CPPTYPE_MESSAGE: # _concrete_class may not yet be initialized. message_type = field.message_type def MakeSubMessageDefault(message): assert getattr(message_type, '_concrete_class', None), ( 'Uninitialized concrete class found for field %r (message type %r)' % (field.full_name, message_type.full_name)) result = message_type._concrete_class() result._SetListener( _OneofListener(message, field) if field.containing_oneof is not None else message._listener_for_children) return result return MakeSubMessageDefault def MakeScalarDefault(message): # TODO(protobuf-team): This may be broken since there may not be # default_value. Combine with has_default_value somehow. return field.default_value return MakeScalarDefault def _ReraiseTypeErrorWithFieldName(message_name, field_name): """Re-raise the currently-handled TypeError with the field name added.""" exc = sys.exc_info()[1] if len(exc.args) == 1 and type(exc) is TypeError: # simple TypeError; add field name to exception message exc = TypeError('%s for field %s.%s' % (str(exc), message_name, field_name)) # re-raise possibly-amended exception with original traceback: raise exc.with_traceback(sys.exc_info()[2]) def _AddInitMethod(message_descriptor, cls): """Adds an __init__ method to cls.""" def _GetIntegerEnumValue(enum_type, value): """Convert a string or integer enum value to an integer. If the value is a string, it is converted to the enum value in enum_type with the same name. If the value is not a string, it's returned as-is. (No conversion or bounds-checking is done.) """ if isinstance(value, str): try: return enum_type.values_by_name[value].number except KeyError: raise ValueError('Enum type %s: unknown label "%s"' % ( enum_type.full_name, value)) return value def init(self, **kwargs): self._cached_byte_size = 0 self._cached_byte_size_dirty = len(kwargs) > 0 self._fields = {} # Contains a mapping from oneof field descriptors to the descriptor # of the currently set field in that oneof field. self._oneofs = {} # _unknown_fields is () when empty for efficiency, and will be turned into # a list if fields are added. self._unknown_fields = () # _unknown_field_set is None when empty for efficiency, and will be # turned into UnknownFieldSet struct if fields are added. self._unknown_field_set = None # pylint: disable=protected-access self._is_present_in_parent = False self._listener = message_listener_mod.NullMessageListener() self._listener_for_children = _Listener(self) for field_name, field_value in kwargs.items(): field = _GetFieldByName(message_descriptor, field_name) if field is None: raise TypeError('%s() got an unexpected keyword argument "%s"' % (message_descriptor.name, field_name)) if field_value is None: # field=None is the same as no field at all. continue if field.label == _FieldDescriptor.LABEL_REPEATED: copy = field._default_constructor(self) if field.cpp_type == _FieldDescriptor.CPPTYPE_MESSAGE: # Composite if _IsMapField(field): if _IsMessageMapField(field): for key in field_value: copy[key].MergeFrom(field_value[key]) else: copy.update(field_value) else: for val in field_value: if isinstance(val, dict): copy.add(**val) else: copy.add().MergeFrom(val) else: # Scalar if field.cpp_type == _FieldDescriptor.CPPTYPE_ENUM: field_value = [_GetIntegerEnumValue(field.enum_type, val) for val in field_value] copy.extend(field_value) self._fields[field] = copy elif field.cpp_type == _FieldDescriptor.CPPTYPE_MESSAGE: copy = field._default_constructor(self) new_val = field_value if isinstance(field_value, dict): new_val = field.message_type._concrete_class(**field_value) try: copy.MergeFrom(new_val) except TypeError: _ReraiseTypeErrorWithFieldName(message_descriptor.name, field_name) self._fields[field] = copy else: if field.cpp_type == _FieldDescriptor.CPPTYPE_ENUM: field_value = _GetIntegerEnumValue(field.enum_type, field_value) try: setattr(self, field_name, field_value) except TypeError: _ReraiseTypeErrorWithFieldName(message_descriptor.name, field_name) init.__module__ = None init.__doc__ = None cls.__init__ = init def _GetFieldByName(message_descriptor, field_name): """Returns a field descriptor by field name. Args: message_descriptor: A Descriptor describing all fields in message. field_name: The name of the field to retrieve. Returns: The field descriptor associated with the field name. """ try: return message_descriptor.fields_by_name[field_name] except KeyError: raise ValueError('Protocol message %s has no "%s" field.' % (message_descriptor.name, field_name)) def _AddPropertiesForFields(descriptor, cls): """Adds properties for all fields in this protocol message type.""" for field in descriptor.fields: _AddPropertiesForField(field, cls) if descriptor.is_extendable: # _ExtensionDict is just an adaptor with no state so we allocate a new one # every time it is accessed. cls.Extensions = property(lambda self: _ExtensionDict(self)) def _AddPropertiesForField(field, cls): """Adds a public property for a protocol message field. Clients can use this property to get and (in the case of non-repeated scalar fields) directly set the value of a protocol message field. Args: field: A FieldDescriptor for this field. cls: The class we're constructing. """ # Catch it if we add other types that we should # handle specially here. assert _FieldDescriptor.MAX_CPPTYPE == 10 constant_name = field.name.upper() + '_FIELD_NUMBER' setattr(cls, constant_name, field.number) if field.label == _FieldDescriptor.LABEL_REPEATED: _AddPropertiesForRepeatedField(field, cls) elif field.cpp_type == _FieldDescriptor.CPPTYPE_MESSAGE: _AddPropertiesForNonRepeatedCompositeField(field, cls) else: _AddPropertiesForNonRepeatedScalarField(field, cls) class _FieldProperty(property): __slots__ = ('DESCRIPTOR',) def __init__(self, descriptor, getter, setter, doc): property.__init__(self, getter, setter, doc=doc) self.DESCRIPTOR = descriptor def _AddPropertiesForRepeatedField(field, cls): """Adds a public property for a "repeated" protocol message field. Clients can use this property to get the value of the field, which will be either a RepeatedScalarFieldContainer or RepeatedCompositeFieldContainer (see below). Note that when clients add values to these containers, we perform type-checking in the case of repeated scalar fields, and we also set any necessary "has" bits as a side-effect. Args: field: A FieldDescriptor for this field. cls: The class we're constructing. """ proto_field_name = field.name property_name = _PropertyName(proto_field_name) def getter(self): field_value = self._fields.get(field) if field_value is None: # Construct a new object to represent this field. field_value = field._default_constructor(self) # Atomically check if another thread has preempted us and, if not, swap # in the new object we just created. If someone has preempted us, we # take that object and discard ours. # WARNING: We are relying on setdefault() being atomic. This is true # in CPython but we haven't investigated others. This warning appears # in several other locations in this file. field_value = self._fields.setdefault(field, field_value) return field_value getter.__module__ = None getter.__doc__ = 'Getter for %s.' % proto_field_name # We define a setter just so we can throw an exception with a more # helpful error message. def setter(self, new_value): raise AttributeError('Assignment not allowed to repeated field ' '"%s" in protocol message object.' % proto_field_name) doc = 'Magic attribute generated for "%s" proto field.' % proto_field_name setattr(cls, property_name, _FieldProperty(field, getter, setter, doc=doc)) def _AddPropertiesForNonRepeatedScalarField(field, cls): """Adds a public property for a nonrepeated, scalar protocol message field. Clients can use this property to get and directly set the value of the field. Note that when the client sets the value of a field by using this property, all necessary "has" bits are set as a side-effect, and we also perform type-checking. Args: field: A FieldDescriptor for this field. cls: The class we're constructing. """ proto_field_name = field.name property_name = _PropertyName(proto_field_name) type_checker = type_checkers.GetTypeChecker(field) default_value = field.default_value is_proto3 = field.containing_type.syntax == 'proto3' def getter(self): # TODO(protobuf-team): This may be broken since there may not be # default_value. Combine with has_default_value somehow. return self._fields.get(field, default_value) getter.__module__ = None getter.__doc__ = 'Getter for %s.' % proto_field_name clear_when_set_to_default = is_proto3 and not field.containing_oneof def field_setter(self, new_value): # pylint: disable=protected-access # Testing the value for truthiness captures all of the proto3 defaults # (0, 0.0, enum 0, and False). try: new_value = type_checker.CheckValue(new_value) except TypeError as e: raise TypeError( 'Cannot set %s to %.1024r: %s' % (field.full_name, new_value, e)) if clear_when_set_to_default and not new_value: self._fields.pop(field, None) else: self._fields[field] = new_value # Check _cached_byte_size_dirty inline to improve performance, since scalar # setters are called frequently. if not self._cached_byte_size_dirty: self._Modified() if field.containing_oneof: def setter(self, new_value): field_setter(self, new_value) self._UpdateOneofState(field) else: setter = field_setter setter.__module__ = None setter.__doc__ = 'Setter for %s.' % proto_field_name # Add a property to encapsulate the getter/setter. doc = 'Magic attribute generated for "%s" proto field.' % proto_field_name setattr(cls, property_name, _FieldProperty(field, getter, setter, doc=doc)) def _AddPropertiesForNonRepeatedCompositeField(field, cls): """Adds a public property for a nonrepeated, composite protocol message field. A composite field is a "group" or "message" field. Clients can use this property to get the value of the field, but cannot assign to the property directly. Args: field: A FieldDescriptor for this field. cls: The class we're constructing. """ # TODO(robinson): Remove duplication with similar method # for non-repeated scalars. proto_field_name = field.name property_name = _PropertyName(proto_field_name) def getter(self): field_value = self._fields.get(field) if field_value is None: # Construct a new object to represent this field. field_value = field._default_constructor(self) # Atomically check if another thread has preempted us and, if not, swap # in the new object we just created. If someone has preempted us, we # take that object and discard ours. # WARNING: We are relying on setdefault() being atomic. This is true # in CPython but we haven't investigated others. This warning appears # in several other locations in this file. field_value = self._fields.setdefault(field, field_value) return field_value getter.__module__ = None getter.__doc__ = 'Getter for %s.' % proto_field_name # We define a setter just so we can throw an exception with a more # helpful error message. def setter(self, new_value): raise AttributeError('Assignment not allowed to composite field ' '"%s" in protocol message object.' % proto_field_name) # Add a property to encapsulate the getter. doc = 'Magic attribute generated for "%s" proto field.' % proto_field_name setattr(cls, property_name, _FieldProperty(field, getter, setter, doc=doc)) def _AddPropertiesForExtensions(descriptor, cls): """Adds properties for all fields in this protocol message type.""" extensions = descriptor.extensions_by_name for extension_name, extension_field in extensions.items(): constant_name = extension_name.upper() + '_FIELD_NUMBER' setattr(cls, constant_name, extension_field.number) # TODO(amauryfa): Migrate all users of these attributes to functions like # pool.FindExtensionByNumber(descriptor). if descriptor.file is not None: # TODO(amauryfa): Use cls.MESSAGE_FACTORY.pool when available. pool = descriptor.file.pool cls._extensions_by_number = pool._extensions_by_number[descriptor] cls._extensions_by_name = pool._extensions_by_name[descriptor] def _AddStaticMethods(cls): # TODO(robinson): This probably needs to be thread-safe(?) def RegisterExtension(extension_handle): extension_handle.containing_type = cls.DESCRIPTOR # TODO(amauryfa): Use cls.MESSAGE_FACTORY.pool when available. # pylint: disable=protected-access cls.DESCRIPTOR.file.pool._AddExtensionDescriptor(extension_handle) _AttachFieldHelpers(cls, extension_handle) cls.RegisterExtension = staticmethod(RegisterExtension) def FromString(s): message = cls() message.MergeFromString(s) return message cls.FromString = staticmethod(FromString) def _IsPresent(item): """Given a (FieldDescriptor, value) tuple from _fields, return true if the value should be included in the list returned by ListFields().""" if item[0].label == _FieldDescriptor.LABEL_REPEATED: return bool(item[1]) elif item[0].cpp_type == _FieldDescriptor.CPPTYPE_MESSAGE: return item[1]._is_present_in_parent else: return True def _AddListFieldsMethod(message_descriptor, cls): """Helper for _AddMessageMethods().""" def ListFields(self): all_fields = [item for item in self._fields.items() if _IsPresent(item)] all_fields.sort(key = lambda item: item[0].number) return all_fields cls.ListFields = ListFields _PROTO3_ERROR_TEMPLATE = \ ('Protocol message %s has no non-repeated submessage field "%s" ' 'nor marked as optional') _PROTO2_ERROR_TEMPLATE = 'Protocol message %s has no non-repeated field "%s"' def _AddHasFieldMethod(message_descriptor, cls): """Helper for _AddMessageMethods().""" is_proto3 = (message_descriptor.syntax == "proto3") error_msg = _PROTO3_ERROR_TEMPLATE if is_proto3 else _PROTO2_ERROR_TEMPLATE hassable_fields = {} for field in message_descriptor.fields: if field.label == _FieldDescriptor.LABEL_REPEATED: continue # For proto3, only submessages and fields inside a oneof have presence. if (is_proto3 and field.cpp_type != _FieldDescriptor.CPPTYPE_MESSAGE and not field.containing_oneof): continue hassable_fields[field.name] = field # Has methods are supported for oneof descriptors. for oneof in message_descriptor.oneofs: hassable_fields[oneof.name] = oneof def HasField(self, field_name): try: field = hassable_fields[field_name] except KeyError: raise ValueError(error_msg % (message_descriptor.full_name, field_name)) if isinstance(field, descriptor_mod.OneofDescriptor): try: return HasField(self, self._oneofs[field].name) except KeyError: return False else: if field.cpp_type == _FieldDescriptor.CPPTYPE_MESSAGE: value = self._fields.get(field) return value is not None and value._is_present_in_parent else: return field in self._fields cls.HasField = HasField def _AddClearFieldMethod(message_descriptor, cls): """Helper for _AddMessageMethods().""" def ClearField(self, field_name): try: field = message_descriptor.fields_by_name[field_name] except KeyError: try: field = message_descriptor.oneofs_by_name[field_name] if field in self._oneofs: field = self._oneofs[field] else: return except KeyError: raise ValueError('Protocol message %s has no "%s" field.' % (message_descriptor.name, field_name)) if field in self._fields: # To match the C++ implementation, we need to invalidate iterators # for map fields when ClearField() happens. if hasattr(self._fields[field], 'InvalidateIterators'): self._fields[field].InvalidateIterators() # Note: If the field is a sub-message, its listener will still point # at us. That's fine, because the worst than can happen is that it # will call _Modified() and invalidate our byte size. Big deal. del self._fields[field] if self._oneofs.get(field.containing_oneof, None) is field: del self._oneofs[field.containing_oneof] # Always call _Modified() -- even if nothing was changed, this is # a mutating method, and thus calling it should cause the field to become # present in the parent message. self._Modified() cls.ClearField = ClearField def _AddClearExtensionMethod(cls): """Helper for _AddMessageMethods().""" def ClearExtension(self, extension_handle): extension_dict._VerifyExtensionHandle(self, extension_handle) # Similar to ClearField(), above. if extension_handle in self._fields: del self._fields[extension_handle] self._Modified() cls.ClearExtension = ClearExtension def _AddHasExtensionMethod(cls): """Helper for _AddMessageMethods().""" def HasExtension(self, extension_handle): extension_dict._VerifyExtensionHandle(self, extension_handle) if extension_handle.label == _FieldDescriptor.LABEL_REPEATED: raise KeyError('"%s" is repeated.' % extension_handle.full_name) if extension_handle.cpp_type == _FieldDescriptor.CPPTYPE_MESSAGE: value = self._fields.get(extension_handle) return value is not None and value._is_present_in_parent else: return extension_handle in self._fields cls.HasExtension = HasExtension def _InternalUnpackAny(msg): """Unpacks Any message and returns the unpacked message. This internal method is different from public Any Unpack method which takes the target message as argument. _InternalUnpackAny method does not have target message type and need to find the message type in descriptor pool. Args: msg: An Any message to be unpacked. Returns: The unpacked message. """ # TODO(amauryfa): Don't use the factory of generated messages. # To make Any work with custom factories, use the message factory of the # parent message. # pylint: disable=g-import-not-at-top from google.protobuf import symbol_database factory = symbol_database.Default() type_url = msg.type_url if not type_url: return None # TODO(haberman): For now we just strip the hostname. Better logic will be # required. type_name = type_url.split('/')[-1] descriptor = factory.pool.FindMessageTypeByName(type_name) if descriptor is None: return None message_class = factory.GetPrototype(descriptor) message = message_class() message.ParseFromString(msg.value) return message def _AddEqualsMethod(message_descriptor, cls): """Helper for _AddMessageMethods().""" def __eq__(self, other): if (not isinstance(other, message_mod.Message) or other.DESCRIPTOR != self.DESCRIPTOR): return False if self is other: return True if self.DESCRIPTOR.full_name == _AnyFullTypeName: any_a = _InternalUnpackAny(self) any_b = _InternalUnpackAny(other) if any_a and any_b: return any_a == any_b if not self.ListFields() == other.ListFields(): return False # TODO(jieluo): Fix UnknownFieldSet to consider MessageSet extensions, # then use it for the comparison. unknown_fields = list(self._unknown_fields) unknown_fields.sort() other_unknown_fields = list(other._unknown_fields) other_unknown_fields.sort() return unknown_fields == other_unknown_fields cls.__eq__ = __eq__ def _AddStrMethod(message_descriptor, cls): """Helper for _AddMessageMethods().""" def __str__(self): return text_format.MessageToString(self) cls.__str__ = __str__ def _AddReprMethod(message_descriptor, cls): """Helper for _AddMessageMethods().""" def __repr__(self): return text_format.MessageToString(self) cls.__repr__ = __repr__ def _AddUnicodeMethod(unused_message_descriptor, cls): """Helper for _AddMessageMethods().""" def __unicode__(self): return text_format.MessageToString(self, as_utf8=True).decode('utf-8') cls.__unicode__ = __unicode__ def _BytesForNonRepeatedElement(value, field_number, field_type): """Returns the number of bytes needed to serialize a non-repeated element. The returned byte count includes space for tag information and any other additional space associated with serializing value. Args: value: Value we're serializing. field_number: Field number of this value. (Since the field number is stored as part of a varint-encoded tag, this has an impact on the total bytes required to serialize the value). field_type: The type of the field. One of the TYPE_* constants within FieldDescriptor. """ try: fn = type_checkers.TYPE_TO_BYTE_SIZE_FN[field_type] return fn(field_number, value) except KeyError: raise message_mod.EncodeError('Unrecognized field type: %d' % field_type) def _AddByteSizeMethod(message_descriptor, cls): """Helper for _AddMessageMethods().""" def ByteSize(self): if not self._cached_byte_size_dirty: return self._cached_byte_size size = 0 descriptor = self.DESCRIPTOR if descriptor.GetOptions().map_entry: # Fields of map entry should always be serialized. size = descriptor.fields_by_name['key']._sizer(self.key) size += descriptor.fields_by_name['value']._sizer(self.value) else: for field_descriptor, field_value in self.ListFields(): size += field_descriptor._sizer(field_value) for tag_bytes, value_bytes in self._unknown_fields: size += len(tag_bytes) + len(value_bytes) self._cached_byte_size = size self._cached_byte_size_dirty = False self._listener_for_children.dirty = False return size cls.ByteSize = ByteSize def _AddSerializeToStringMethod(message_descriptor, cls): """Helper for _AddMessageMethods().""" def SerializeToString(self, **kwargs): # Check if the message has all of its required fields set. if not self.IsInitialized(): raise message_mod.EncodeError( 'Message %s is missing required fields: %s' % ( self.DESCRIPTOR.full_name, ','.join(self.FindInitializationErrors()))) return self.SerializePartialToString(**kwargs) cls.SerializeToString = SerializeToString def _AddSerializePartialToStringMethod(message_descriptor, cls): """Helper for _AddMessageMethods().""" def SerializePartialToString(self, **kwargs): out = BytesIO() self._InternalSerialize(out.write, **kwargs) return out.getvalue() cls.SerializePartialToString = SerializePartialToString def InternalSerialize(self, write_bytes, deterministic=None): if deterministic is None: deterministic = ( api_implementation.IsPythonDefaultSerializationDeterministic()) else: deterministic = bool(deterministic) descriptor = self.DESCRIPTOR if descriptor.GetOptions().map_entry: # Fields of map entry should always be serialized. descriptor.fields_by_name['key']._encoder( write_bytes, self.key, deterministic) descriptor.fields_by_name['value']._encoder( write_bytes, self.value, deterministic) else: for field_descriptor, field_value in self.ListFields(): field_descriptor._encoder(write_bytes, field_value, deterministic) for tag_bytes, value_bytes in self._unknown_fields: write_bytes(tag_bytes) write_bytes(value_bytes) cls._InternalSerialize = InternalSerialize def _AddMergeFromStringMethod(message_descriptor, cls): """Helper for _AddMessageMethods().""" def MergeFromString(self, serialized): serialized = memoryview(serialized) length = len(serialized) try: if self._InternalParse(serialized, 0, length) != length: # The only reason _InternalParse would return early is if it # encountered an end-group tag. raise message_mod.DecodeError('Unexpected end-group tag.') except (IndexError, TypeError): # Now ord(buf[p:p+1]) == ord('') gets TypeError. raise message_mod.DecodeError('Truncated message.') except struct.error as e: raise message_mod.DecodeError(e) return length # Return this for legacy reasons. cls.MergeFromString = MergeFromString local_ReadTag = decoder.ReadTag local_SkipField = decoder.SkipField decoders_by_tag = cls._decoders_by_tag def InternalParse(self, buffer, pos, end): """Create a message from serialized bytes. Args: self: Message, instance of the proto message object. buffer: memoryview of the serialized data. pos: int, position to start in the serialized data. end: int, end position of the serialized data. Returns: Message object. """ # Guard against internal misuse, since this function is called internally # quite extensively, and its easy to accidentally pass bytes. assert isinstance(buffer, memoryview) self._Modified() field_dict = self._fields # pylint: disable=protected-access unknown_field_set = self._unknown_field_set while pos != end: (tag_bytes, new_pos) = local_ReadTag(buffer, pos) field_decoder, field_desc = decoders_by_tag.get(tag_bytes, (None, None)) if field_decoder is None: if not self._unknown_fields: # pylint: disable=protected-access self._unknown_fields = [] # pylint: disable=protected-access if unknown_field_set is None: # pylint: disable=protected-access self._unknown_field_set = containers.UnknownFieldSet() # pylint: disable=protected-access unknown_field_set = self._unknown_field_set # pylint: disable=protected-access (tag, _) = decoder._DecodeVarint(tag_bytes, 0) field_number, wire_type = wire_format.UnpackTag(tag) if field_number == 0: raise message_mod.DecodeError('Field number 0 is illegal.') # TODO(jieluo): remove old_pos. old_pos = new_pos (data, new_pos) = decoder._DecodeUnknownField( buffer, new_pos, wire_type) # pylint: disable=protected-access if new_pos == -1: return pos # pylint: disable=protected-access unknown_field_set._add(field_number, wire_type, data) # TODO(jieluo): remove _unknown_fields. new_pos = local_SkipField(buffer, old_pos, end, tag_bytes) if new_pos == -1: return pos self._unknown_fields.append( (tag_bytes, buffer[old_pos:new_pos].tobytes())) pos = new_pos else: pos = field_decoder(buffer, new_pos, end, self, field_dict) if field_desc: self._UpdateOneofState(field_desc) return pos cls._InternalParse = InternalParse def _AddIsInitializedMethod(message_descriptor, cls): """Adds the IsInitialized and FindInitializationError methods to the protocol message class.""" required_fields = [field for field in message_descriptor.fields if field.label == _FieldDescriptor.LABEL_REQUIRED] def IsInitialized(self, errors=None): """Checks if all required fields of a message are set. Args: errors: A list which, if provided, will be populated with the field paths of all missing required fields. Returns: True iff the specified message has all required fields set. """ # Performance is critical so we avoid HasField() and ListFields(). for field in required_fields: if (field not in self._fields or (field.cpp_type == _FieldDescriptor.CPPTYPE_MESSAGE and not self._fields[field]._is_present_in_parent)): if errors is not None: errors.extend(self.FindInitializationErrors()) return False for field, value in list(self._fields.items()): # dict can change size! if field.cpp_type == _FieldDescriptor.CPPTYPE_MESSAGE: if field.label == _FieldDescriptor.LABEL_REPEATED: if (field.message_type.has_options and field.message_type.GetOptions().map_entry): continue for element in value: if not element.IsInitialized(): if errors is not None: errors.extend(self.FindInitializationErrors()) return False elif value._is_present_in_parent and not value.IsInitialized(): if errors is not None: errors.extend(self.FindInitializationErrors()) return False return True cls.IsInitialized = IsInitialized def FindInitializationErrors(self): """Finds required fields which are not initialized. Returns: A list of strings. Each string is a path to an uninitialized field from the top-level message, e.g. "foo.bar[5].baz". """ errors = [] # simplify things for field in required_fields: if not self.HasField(field.name): errors.append(field.name) for field, value in self.ListFields(): if field.cpp_type == _FieldDescriptor.CPPTYPE_MESSAGE: if field.is_extension: name = '(%s)' % field.full_name else: name = field.name if _IsMapField(field): if _IsMessageMapField(field): for key in value: element = value[key] prefix = '%s[%s].' % (name, key) sub_errors = element.FindInitializationErrors() errors += [prefix + error for error in sub_errors] else: # ScalarMaps can't have any initialization errors. pass elif field.label == _FieldDescriptor.LABEL_REPEATED: for i in range(len(value)): element = value[i] prefix = '%s[%d].' % (name, i) sub_errors = element.FindInitializationErrors() errors += [prefix + error for error in sub_errors] else: prefix = name + '.' sub_errors = value.FindInitializationErrors() errors += [prefix + error for error in sub_errors] return errors cls.FindInitializationErrors = FindInitializationErrors def _FullyQualifiedClassName(klass): module = klass.__module__ name = getattr(klass, '__qualname__', klass.__name__) if module in (None, 'builtins', '__builtin__'): return name return module + '.' + name def _AddMergeFromMethod(cls): LABEL_REPEATED = _FieldDescriptor.LABEL_REPEATED CPPTYPE_MESSAGE = _FieldDescriptor.CPPTYPE_MESSAGE def MergeFrom(self, msg): if not isinstance(msg, cls): raise TypeError( 'Parameter to MergeFrom() must be instance of same class: ' 'expected %s got %s.' % (_FullyQualifiedClassName(cls), _FullyQualifiedClassName(msg.__class__))) assert msg is not self self._Modified() fields = self._fields for field, value in msg._fields.items(): if field.label == LABEL_REPEATED: field_value = fields.get(field) if field_value is None: # Construct a new object to represent this field. field_value = field._default_constructor(self) fields[field] = field_value field_value.MergeFrom(value) elif field.cpp_type == CPPTYPE_MESSAGE: if value._is_present_in_parent: field_value = fields.get(field) if field_value is None: # Construct a new object to represent this field. field_value = field._default_constructor(self) fields[field] = field_value field_value.MergeFrom(value) else: self._fields[field] = value if field.containing_oneof: self._UpdateOneofState(field) if msg._unknown_fields: if not self._unknown_fields: self._unknown_fields = [] self._unknown_fields.extend(msg._unknown_fields) # pylint: disable=protected-access if self._unknown_field_set is None: self._unknown_field_set = containers.UnknownFieldSet() self._unknown_field_set._extend(msg._unknown_field_set) cls.MergeFrom = MergeFrom def _AddWhichOneofMethod(message_descriptor, cls): def WhichOneof(self, oneof_name): """Returns the name of the currently set field inside a oneof, or None.""" try: field = message_descriptor.oneofs_by_name[oneof_name] except KeyError: raise ValueError( 'Protocol message has no oneof "%s" field.' % oneof_name) nested_field = self._oneofs.get(field, None) if nested_field is not None and self.HasField(nested_field.name): return nested_field.name else: return None cls.WhichOneof = WhichOneof def _Clear(self): # Clear fields. self._fields = {} self._unknown_fields = () # pylint: disable=protected-access if self._unknown_field_set is not None: self._unknown_field_set._clear() self._unknown_field_set = None self._oneofs = {} self._Modified() def _UnknownFields(self): if self._unknown_field_set is None: # pylint: disable=protected-access # pylint: disable=protected-access self._unknown_field_set = containers.UnknownFieldSet() return self._unknown_field_set # pylint: disable=protected-access def _DiscardUnknownFields(self): self._unknown_fields = [] self._unknown_field_set = None # pylint: disable=protected-access for field, value in self.ListFields(): if field.cpp_type == _FieldDescriptor.CPPTYPE_MESSAGE: if _IsMapField(field): if _IsMessageMapField(field): for key in value: value[key].DiscardUnknownFields() elif field.label == _FieldDescriptor.LABEL_REPEATED: for sub_message in value: sub_message.DiscardUnknownFields() else: value.DiscardUnknownFields() def _SetListener(self, listener): if listener is None: self._listener = message_listener_mod.NullMessageListener() else: self._listener = listener def _AddMessageMethods(message_descriptor, cls): """Adds implementations of all Message methods to cls.""" _AddListFieldsMethod(message_descriptor, cls) _AddHasFieldMethod(message_descriptor, cls) _AddClearFieldMethod(message_descriptor, cls) if message_descriptor.is_extendable: _AddClearExtensionMethod(cls) _AddHasExtensionMethod(cls) _AddEqualsMethod(message_descriptor, cls) _AddStrMethod(message_descriptor, cls) _AddReprMethod(message_descriptor, cls) _AddUnicodeMethod(message_descriptor, cls) _AddByteSizeMethod(message_descriptor, cls) _AddSerializeToStringMethod(message_descriptor, cls) _AddSerializePartialToStringMethod(message_descriptor, cls) _AddMergeFromStringMethod(message_descriptor, cls) _AddIsInitializedMethod(message_descriptor, cls) _AddMergeFromMethod(cls) _AddWhichOneofMethod(message_descriptor, cls) # Adds methods which do not depend on cls. cls.Clear = _Clear cls.UnknownFields = _UnknownFields cls.DiscardUnknownFields = _DiscardUnknownFields cls._SetListener = _SetListener def _AddPrivateHelperMethods(message_descriptor, cls): """Adds implementation of private helper methods to cls.""" def Modified(self): """Sets the _cached_byte_size_dirty bit to true, and propagates this to our listener iff this was a state change. """ # Note: Some callers check _cached_byte_size_dirty before calling # _Modified() as an extra optimization. So, if this method is ever # changed such that it does stuff even when _cached_byte_size_dirty is # already true, the callers need to be updated. if not self._cached_byte_size_dirty: self._cached_byte_size_dirty = True self._listener_for_children.dirty = True self._is_present_in_parent = True self._listener.Modified() def _UpdateOneofState(self, field): """Sets field as the active field in its containing oneof. Will also delete currently active field in the oneof, if it is different from the argument. Does not mark the message as modified. """ other_field = self._oneofs.setdefault(field.containing_oneof, field) if other_field is not field: del self._fields[other_field] self._oneofs[field.containing_oneof] = field cls._Modified = Modified cls.SetInParent = Modified cls._UpdateOneofState = _UpdateOneofState class _Listener(object): """MessageListener implementation that a parent message registers with its child message. In order to support semantics like: foo.bar.baz.qux = 23 assert foo.HasField('bar') ...child objects must have back references to their parents. This helper class is at the heart of this support. """ def __init__(self, parent_message): """Args: parent_message: The message whose _Modified() method we should call when we receive Modified() messages. """ # This listener establishes a back reference from a child (contained) object # to its parent (containing) object. We make this a weak reference to avoid # creating cyclic garbage when the client finishes with the 'parent' object # in the tree. if isinstance(parent_message, weakref.ProxyType): self._parent_message_weakref = parent_message else: self._parent_message_weakref = weakref.proxy(parent_message) # As an optimization, we also indicate directly on the listener whether # or not the parent message is dirty. This way we can avoid traversing # up the tree in the common case. self.dirty = False def Modified(self): if self.dirty: return try: # Propagate the signal to our parents iff this is the first field set. self._parent_message_weakref._Modified() except ReferenceError: # We can get here if a client has kept a reference to a child object, # and is now setting a field on it, but the child's parent has been # garbage-collected. This is not an error. pass class _OneofListener(_Listener): """Special listener implementation for setting composite oneof fields.""" def __init__(self, parent_message, field): """Args: parent_message: The message whose _Modified() method we should call when we receive Modified() messages. field: The descriptor of the field being set in the parent message. """ super(_OneofListener, self).__init__(parent_message) self._field = field def Modified(self): """Also updates the state of the containing oneof in the parent message.""" try: self._parent_message_weakref._UpdateOneofState(self._field) super(_OneofListener, self).Modified() except ReferenceError: pass ================================================ FILE: openpype/hosts/hiero/vendor/google/protobuf/internal/type_checkers.py ================================================ # Protocol Buffers - Google's data interchange format # Copyright 2008 Google Inc. All rights reserved. # https://developers.google.com/protocol-buffers/ # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are # met: # # * Redistributions of source code must retain the above copyright # notice, this list of conditions and the following disclaimer. # * Redistributions in binary form must reproduce the above # copyright notice, this list of conditions and the following disclaimer # in the documentation and/or other materials provided with the # distribution. # * Neither the name of Google Inc. nor the names of its # contributors may be used to endorse or promote products derived from # this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. """Provides type checking routines. This module defines type checking utilities in the forms of dictionaries: VALUE_CHECKERS: A dictionary of field types and a value validation object. TYPE_TO_BYTE_SIZE_FN: A dictionary with field types and a size computing function. TYPE_TO_SERIALIZE_METHOD: A dictionary with field types and serialization function. FIELD_TYPE_TO_WIRE_TYPE: A dictionary with field typed and their corresponding wire types. TYPE_TO_DESERIALIZE_METHOD: A dictionary with field types and deserialization function. """ __author__ = 'robinson@google.com (Will Robinson)' import ctypes import numbers from google.protobuf.internal import decoder from google.protobuf.internal import encoder from google.protobuf.internal import wire_format from google.protobuf import descriptor _FieldDescriptor = descriptor.FieldDescriptor def TruncateToFourByteFloat(original): return ctypes.c_float(original).value def ToShortestFloat(original): """Returns the shortest float that has same value in wire.""" # All 4 byte floats have between 6 and 9 significant digits, so we # start with 6 as the lower bound. # It has to be iterative because use '.9g' directly can not get rid # of the noises for most values. For example if set a float_field=0.9 # use '.9g' will print 0.899999976. precision = 6 rounded = float('{0:.{1}g}'.format(original, precision)) while TruncateToFourByteFloat(rounded) != original: precision += 1 rounded = float('{0:.{1}g}'.format(original, precision)) return rounded def SupportsOpenEnums(field_descriptor): return field_descriptor.containing_type.syntax == 'proto3' def GetTypeChecker(field): """Returns a type checker for a message field of the specified types. Args: field: FieldDescriptor object for this field. Returns: An instance of TypeChecker which can be used to verify the types of values assigned to a field of the specified type. """ if (field.cpp_type == _FieldDescriptor.CPPTYPE_STRING and field.type == _FieldDescriptor.TYPE_STRING): return UnicodeValueChecker() if field.cpp_type == _FieldDescriptor.CPPTYPE_ENUM: if SupportsOpenEnums(field): # When open enums are supported, any int32 can be assigned. return _VALUE_CHECKERS[_FieldDescriptor.CPPTYPE_INT32] else: return EnumValueChecker(field.enum_type) return _VALUE_CHECKERS[field.cpp_type] # None of the typecheckers below make any attempt to guard against people # subclassing builtin types and doing weird things. We're not trying to # protect against malicious clients here, just people accidentally shooting # themselves in the foot in obvious ways. class TypeChecker(object): """Type checker used to catch type errors as early as possible when the client is setting scalar fields in protocol messages. """ def __init__(self, *acceptable_types): self._acceptable_types = acceptable_types def CheckValue(self, proposed_value): """Type check the provided value and return it. The returned value might have been normalized to another type. """ if not isinstance(proposed_value, self._acceptable_types): message = ('%.1024r has type %s, but expected one of: %s' % (proposed_value, type(proposed_value), self._acceptable_types)) raise TypeError(message) return proposed_value class TypeCheckerWithDefault(TypeChecker): def __init__(self, default_value, *acceptable_types): TypeChecker.__init__(self, *acceptable_types) self._default_value = default_value def DefaultValue(self): return self._default_value class BoolValueChecker(object): """Type checker used for bool fields.""" def CheckValue(self, proposed_value): if not hasattr(proposed_value, '__index__') or ( type(proposed_value).__module__ == 'numpy' and type(proposed_value).__name__ == 'ndarray'): message = ('%.1024r has type %s, but expected one of: %s' % (proposed_value, type(proposed_value), (bool, int))) raise TypeError(message) return bool(proposed_value) def DefaultValue(self): return False # IntValueChecker and its subclasses perform integer type-checks # and bounds-checks. class IntValueChecker(object): """Checker used for integer fields. Performs type-check and range check.""" def CheckValue(self, proposed_value): if not hasattr(proposed_value, '__index__') or ( type(proposed_value).__module__ == 'numpy' and type(proposed_value).__name__ == 'ndarray'): message = ('%.1024r has type %s, but expected one of: %s' % (proposed_value, type(proposed_value), (int,))) raise TypeError(message) if not self._MIN <= int(proposed_value) <= self._MAX: raise ValueError('Value out of range: %d' % proposed_value) # We force all values to int to make alternate implementations where the # distinction is more significant (e.g. the C++ implementation) simpler. proposed_value = int(proposed_value) return proposed_value def DefaultValue(self): return 0 class EnumValueChecker(object): """Checker used for enum fields. Performs type-check and range check.""" def __init__(self, enum_type): self._enum_type = enum_type def CheckValue(self, proposed_value): if not isinstance(proposed_value, numbers.Integral): message = ('%.1024r has type %s, but expected one of: %s' % (proposed_value, type(proposed_value), (int,))) raise TypeError(message) if int(proposed_value) not in self._enum_type.values_by_number: raise ValueError('Unknown enum value: %d' % proposed_value) return proposed_value def DefaultValue(self): return self._enum_type.values[0].number class UnicodeValueChecker(object): """Checker used for string fields. Always returns a unicode value, even if the input is of type str. """ def CheckValue(self, proposed_value): if not isinstance(proposed_value, (bytes, str)): message = ('%.1024r has type %s, but expected one of: %s' % (proposed_value, type(proposed_value), (bytes, str))) raise TypeError(message) # If the value is of type 'bytes' make sure that it is valid UTF-8 data. if isinstance(proposed_value, bytes): try: proposed_value = proposed_value.decode('utf-8') except UnicodeDecodeError: raise ValueError('%.1024r has type bytes, but isn\'t valid UTF-8 ' 'encoding. Non-UTF-8 strings must be converted to ' 'unicode objects before being added.' % (proposed_value)) else: try: proposed_value.encode('utf8') except UnicodeEncodeError: raise ValueError('%.1024r isn\'t a valid unicode string and ' 'can\'t be encoded in UTF-8.'% (proposed_value)) return proposed_value def DefaultValue(self): return u"" class Int32ValueChecker(IntValueChecker): # We're sure to use ints instead of longs here since comparison may be more # efficient. _MIN = -2147483648 _MAX = 2147483647 class Uint32ValueChecker(IntValueChecker): _MIN = 0 _MAX = (1 << 32) - 1 class Int64ValueChecker(IntValueChecker): _MIN = -(1 << 63) _MAX = (1 << 63) - 1 class Uint64ValueChecker(IntValueChecker): _MIN = 0 _MAX = (1 << 64) - 1 # The max 4 bytes float is about 3.4028234663852886e+38 _FLOAT_MAX = float.fromhex('0x1.fffffep+127') _FLOAT_MIN = -_FLOAT_MAX _INF = float('inf') _NEG_INF = float('-inf') class DoubleValueChecker(object): """Checker used for double fields. Performs type-check and range check. """ def CheckValue(self, proposed_value): """Check and convert proposed_value to float.""" if (not hasattr(proposed_value, '__float__') and not hasattr(proposed_value, '__index__')) or ( type(proposed_value).__module__ == 'numpy' and type(proposed_value).__name__ == 'ndarray'): message = ('%.1024r has type %s, but expected one of: int, float' % (proposed_value, type(proposed_value))) raise TypeError(message) return float(proposed_value) def DefaultValue(self): return 0.0 class FloatValueChecker(DoubleValueChecker): """Checker used for float fields. Performs type-check and range check. Values exceeding a 32-bit float will be converted to inf/-inf. """ def CheckValue(self, proposed_value): """Check and convert proposed_value to float.""" converted_value = super().CheckValue(proposed_value) # This inf rounding matches the C++ proto SafeDoubleToFloat logic. if converted_value > _FLOAT_MAX: return _INF if converted_value < _FLOAT_MIN: return _NEG_INF return TruncateToFourByteFloat(converted_value) # Type-checkers for all scalar CPPTYPEs. _VALUE_CHECKERS = { _FieldDescriptor.CPPTYPE_INT32: Int32ValueChecker(), _FieldDescriptor.CPPTYPE_INT64: Int64ValueChecker(), _FieldDescriptor.CPPTYPE_UINT32: Uint32ValueChecker(), _FieldDescriptor.CPPTYPE_UINT64: Uint64ValueChecker(), _FieldDescriptor.CPPTYPE_DOUBLE: DoubleValueChecker(), _FieldDescriptor.CPPTYPE_FLOAT: FloatValueChecker(), _FieldDescriptor.CPPTYPE_BOOL: BoolValueChecker(), _FieldDescriptor.CPPTYPE_STRING: TypeCheckerWithDefault(b'', bytes), } # Map from field type to a function F, such that F(field_num, value) # gives the total byte size for a value of the given type. This # byte size includes tag information and any other additional space # associated with serializing "value". TYPE_TO_BYTE_SIZE_FN = { _FieldDescriptor.TYPE_DOUBLE: wire_format.DoubleByteSize, _FieldDescriptor.TYPE_FLOAT: wire_format.FloatByteSize, _FieldDescriptor.TYPE_INT64: wire_format.Int64ByteSize, _FieldDescriptor.TYPE_UINT64: wire_format.UInt64ByteSize, _FieldDescriptor.TYPE_INT32: wire_format.Int32ByteSize, _FieldDescriptor.TYPE_FIXED64: wire_format.Fixed64ByteSize, _FieldDescriptor.TYPE_FIXED32: wire_format.Fixed32ByteSize, _FieldDescriptor.TYPE_BOOL: wire_format.BoolByteSize, _FieldDescriptor.TYPE_STRING: wire_format.StringByteSize, _FieldDescriptor.TYPE_GROUP: wire_format.GroupByteSize, _FieldDescriptor.TYPE_MESSAGE: wire_format.MessageByteSize, _FieldDescriptor.TYPE_BYTES: wire_format.BytesByteSize, _FieldDescriptor.TYPE_UINT32: wire_format.UInt32ByteSize, _FieldDescriptor.TYPE_ENUM: wire_format.EnumByteSize, _FieldDescriptor.TYPE_SFIXED32: wire_format.SFixed32ByteSize, _FieldDescriptor.TYPE_SFIXED64: wire_format.SFixed64ByteSize, _FieldDescriptor.TYPE_SINT32: wire_format.SInt32ByteSize, _FieldDescriptor.TYPE_SINT64: wire_format.SInt64ByteSize } # Maps from field types to encoder constructors. TYPE_TO_ENCODER = { _FieldDescriptor.TYPE_DOUBLE: encoder.DoubleEncoder, _FieldDescriptor.TYPE_FLOAT: encoder.FloatEncoder, _FieldDescriptor.TYPE_INT64: encoder.Int64Encoder, _FieldDescriptor.TYPE_UINT64: encoder.UInt64Encoder, _FieldDescriptor.TYPE_INT32: encoder.Int32Encoder, _FieldDescriptor.TYPE_FIXED64: encoder.Fixed64Encoder, _FieldDescriptor.TYPE_FIXED32: encoder.Fixed32Encoder, _FieldDescriptor.TYPE_BOOL: encoder.BoolEncoder, _FieldDescriptor.TYPE_STRING: encoder.StringEncoder, _FieldDescriptor.TYPE_GROUP: encoder.GroupEncoder, _FieldDescriptor.TYPE_MESSAGE: encoder.MessageEncoder, _FieldDescriptor.TYPE_BYTES: encoder.BytesEncoder, _FieldDescriptor.TYPE_UINT32: encoder.UInt32Encoder, _FieldDescriptor.TYPE_ENUM: encoder.EnumEncoder, _FieldDescriptor.TYPE_SFIXED32: encoder.SFixed32Encoder, _FieldDescriptor.TYPE_SFIXED64: encoder.SFixed64Encoder, _FieldDescriptor.TYPE_SINT32: encoder.SInt32Encoder, _FieldDescriptor.TYPE_SINT64: encoder.SInt64Encoder, } # Maps from field types to sizer constructors. TYPE_TO_SIZER = { _FieldDescriptor.TYPE_DOUBLE: encoder.DoubleSizer, _FieldDescriptor.TYPE_FLOAT: encoder.FloatSizer, _FieldDescriptor.TYPE_INT64: encoder.Int64Sizer, _FieldDescriptor.TYPE_UINT64: encoder.UInt64Sizer, _FieldDescriptor.TYPE_INT32: encoder.Int32Sizer, _FieldDescriptor.TYPE_FIXED64: encoder.Fixed64Sizer, _FieldDescriptor.TYPE_FIXED32: encoder.Fixed32Sizer, _FieldDescriptor.TYPE_BOOL: encoder.BoolSizer, _FieldDescriptor.TYPE_STRING: encoder.StringSizer, _FieldDescriptor.TYPE_GROUP: encoder.GroupSizer, _FieldDescriptor.TYPE_MESSAGE: encoder.MessageSizer, _FieldDescriptor.TYPE_BYTES: encoder.BytesSizer, _FieldDescriptor.TYPE_UINT32: encoder.UInt32Sizer, _FieldDescriptor.TYPE_ENUM: encoder.EnumSizer, _FieldDescriptor.TYPE_SFIXED32: encoder.SFixed32Sizer, _FieldDescriptor.TYPE_SFIXED64: encoder.SFixed64Sizer, _FieldDescriptor.TYPE_SINT32: encoder.SInt32Sizer, _FieldDescriptor.TYPE_SINT64: encoder.SInt64Sizer, } # Maps from field type to a decoder constructor. TYPE_TO_DECODER = { _FieldDescriptor.TYPE_DOUBLE: decoder.DoubleDecoder, _FieldDescriptor.TYPE_FLOAT: decoder.FloatDecoder, _FieldDescriptor.TYPE_INT64: decoder.Int64Decoder, _FieldDescriptor.TYPE_UINT64: decoder.UInt64Decoder, _FieldDescriptor.TYPE_INT32: decoder.Int32Decoder, _FieldDescriptor.TYPE_FIXED64: decoder.Fixed64Decoder, _FieldDescriptor.TYPE_FIXED32: decoder.Fixed32Decoder, _FieldDescriptor.TYPE_BOOL: decoder.BoolDecoder, _FieldDescriptor.TYPE_STRING: decoder.StringDecoder, _FieldDescriptor.TYPE_GROUP: decoder.GroupDecoder, _FieldDescriptor.TYPE_MESSAGE: decoder.MessageDecoder, _FieldDescriptor.TYPE_BYTES: decoder.BytesDecoder, _FieldDescriptor.TYPE_UINT32: decoder.UInt32Decoder, _FieldDescriptor.TYPE_ENUM: decoder.EnumDecoder, _FieldDescriptor.TYPE_SFIXED32: decoder.SFixed32Decoder, _FieldDescriptor.TYPE_SFIXED64: decoder.SFixed64Decoder, _FieldDescriptor.TYPE_SINT32: decoder.SInt32Decoder, _FieldDescriptor.TYPE_SINT64: decoder.SInt64Decoder, } # Maps from field type to expected wiretype. FIELD_TYPE_TO_WIRE_TYPE = { _FieldDescriptor.TYPE_DOUBLE: wire_format.WIRETYPE_FIXED64, _FieldDescriptor.TYPE_FLOAT: wire_format.WIRETYPE_FIXED32, _FieldDescriptor.TYPE_INT64: wire_format.WIRETYPE_VARINT, _FieldDescriptor.TYPE_UINT64: wire_format.WIRETYPE_VARINT, _FieldDescriptor.TYPE_INT32: wire_format.WIRETYPE_VARINT, _FieldDescriptor.TYPE_FIXED64: wire_format.WIRETYPE_FIXED64, _FieldDescriptor.TYPE_FIXED32: wire_format.WIRETYPE_FIXED32, _FieldDescriptor.TYPE_BOOL: wire_format.WIRETYPE_VARINT, _FieldDescriptor.TYPE_STRING: wire_format.WIRETYPE_LENGTH_DELIMITED, _FieldDescriptor.TYPE_GROUP: wire_format.WIRETYPE_START_GROUP, _FieldDescriptor.TYPE_MESSAGE: wire_format.WIRETYPE_LENGTH_DELIMITED, _FieldDescriptor.TYPE_BYTES: wire_format.WIRETYPE_LENGTH_DELIMITED, _FieldDescriptor.TYPE_UINT32: wire_format.WIRETYPE_VARINT, _FieldDescriptor.TYPE_ENUM: wire_format.WIRETYPE_VARINT, _FieldDescriptor.TYPE_SFIXED32: wire_format.WIRETYPE_FIXED32, _FieldDescriptor.TYPE_SFIXED64: wire_format.WIRETYPE_FIXED64, _FieldDescriptor.TYPE_SINT32: wire_format.WIRETYPE_VARINT, _FieldDescriptor.TYPE_SINT64: wire_format.WIRETYPE_VARINT, } ================================================ FILE: openpype/hosts/hiero/vendor/google/protobuf/internal/well_known_types.py ================================================ # Protocol Buffers - Google's data interchange format # Copyright 2008 Google Inc. All rights reserved. # https://developers.google.com/protocol-buffers/ # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are # met: # # * Redistributions of source code must retain the above copyright # notice, this list of conditions and the following disclaimer. # * Redistributions in binary form must reproduce the above # copyright notice, this list of conditions and the following disclaimer # in the documentation and/or other materials provided with the # distribution. # * Neither the name of Google Inc. nor the names of its # contributors may be used to endorse or promote products derived from # this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. """Contains well known classes. This files defines well known classes which need extra maintenance including: - Any - Duration - FieldMask - Struct - Timestamp """ __author__ = 'jieluo@google.com (Jie Luo)' import calendar import collections.abc import datetime from google.protobuf.descriptor import FieldDescriptor _TIMESTAMPFOMAT = '%Y-%m-%dT%H:%M:%S' _NANOS_PER_SECOND = 1000000000 _NANOS_PER_MILLISECOND = 1000000 _NANOS_PER_MICROSECOND = 1000 _MILLIS_PER_SECOND = 1000 _MICROS_PER_SECOND = 1000000 _SECONDS_PER_DAY = 24 * 3600 _DURATION_SECONDS_MAX = 315576000000 class Any(object): """Class for Any Message type.""" __slots__ = () def Pack(self, msg, type_url_prefix='type.googleapis.com/', deterministic=None): """Packs the specified message into current Any message.""" if len(type_url_prefix) < 1 or type_url_prefix[-1] != '/': self.type_url = '%s/%s' % (type_url_prefix, msg.DESCRIPTOR.full_name) else: self.type_url = '%s%s' % (type_url_prefix, msg.DESCRIPTOR.full_name) self.value = msg.SerializeToString(deterministic=deterministic) def Unpack(self, msg): """Unpacks the current Any message into specified message.""" descriptor = msg.DESCRIPTOR if not self.Is(descriptor): return False msg.ParseFromString(self.value) return True def TypeName(self): """Returns the protobuf type name of the inner message.""" # Only last part is to be used: b/25630112 return self.type_url.split('/')[-1] def Is(self, descriptor): """Checks if this Any represents the given protobuf type.""" return '/' in self.type_url and self.TypeName() == descriptor.full_name _EPOCH_DATETIME_NAIVE = datetime.datetime.utcfromtimestamp(0) _EPOCH_DATETIME_AWARE = datetime.datetime.fromtimestamp( 0, tz=datetime.timezone.utc) class Timestamp(object): """Class for Timestamp message type.""" __slots__ = () def ToJsonString(self): """Converts Timestamp to RFC 3339 date string format. Returns: A string converted from timestamp. The string is always Z-normalized and uses 3, 6 or 9 fractional digits as required to represent the exact time. Example of the return format: '1972-01-01T10:00:20.021Z' """ nanos = self.nanos % _NANOS_PER_SECOND total_sec = self.seconds + (self.nanos - nanos) // _NANOS_PER_SECOND seconds = total_sec % _SECONDS_PER_DAY days = (total_sec - seconds) // _SECONDS_PER_DAY dt = datetime.datetime(1970, 1, 1) + datetime.timedelta(days, seconds) result = dt.isoformat() if (nanos % 1e9) == 0: # If there are 0 fractional digits, the fractional # point '.' should be omitted when serializing. return result + 'Z' if (nanos % 1e6) == 0: # Serialize 3 fractional digits. return result + '.%03dZ' % (nanos / 1e6) if (nanos % 1e3) == 0: # Serialize 6 fractional digits. return result + '.%06dZ' % (nanos / 1e3) # Serialize 9 fractional digits. return result + '.%09dZ' % nanos def FromJsonString(self, value): """Parse a RFC 3339 date string format to Timestamp. Args: value: A date string. Any fractional digits (or none) and any offset are accepted as long as they fit into nano-seconds precision. Example of accepted format: '1972-01-01T10:00:20.021-05:00' Raises: ValueError: On parsing problems. """ if not isinstance(value, str): raise ValueError('Timestamp JSON value not a string: {!r}'.format(value)) timezone_offset = value.find('Z') if timezone_offset == -1: timezone_offset = value.find('+') if timezone_offset == -1: timezone_offset = value.rfind('-') if timezone_offset == -1: raise ValueError( 'Failed to parse timestamp: missing valid timezone offset.') time_value = value[0:timezone_offset] # Parse datetime and nanos. point_position = time_value.find('.') if point_position == -1: second_value = time_value nano_value = '' else: second_value = time_value[:point_position] nano_value = time_value[point_position + 1:] if 't' in second_value: raise ValueError( 'time data \'{0}\' does not match format \'%Y-%m-%dT%H:%M:%S\', ' 'lowercase \'t\' is not accepted'.format(second_value)) date_object = datetime.datetime.strptime(second_value, _TIMESTAMPFOMAT) td = date_object - datetime.datetime(1970, 1, 1) seconds = td.seconds + td.days * _SECONDS_PER_DAY if len(nano_value) > 9: raise ValueError( 'Failed to parse Timestamp: nanos {0} more than ' '9 fractional digits.'.format(nano_value)) if nano_value: nanos = round(float('0.' + nano_value) * 1e9) else: nanos = 0 # Parse timezone offsets. if value[timezone_offset] == 'Z': if len(value) != timezone_offset + 1: raise ValueError('Failed to parse timestamp: invalid trailing' ' data {0}.'.format(value)) else: timezone = value[timezone_offset:] pos = timezone.find(':') if pos == -1: raise ValueError( 'Invalid timezone offset value: {0}.'.format(timezone)) if timezone[0] == '+': seconds -= (int(timezone[1:pos])*60+int(timezone[pos+1:]))*60 else: seconds += (int(timezone[1:pos])*60+int(timezone[pos+1:]))*60 # Set seconds and nanos self.seconds = int(seconds) self.nanos = int(nanos) def GetCurrentTime(self): """Get the current UTC into Timestamp.""" self.FromDatetime(datetime.datetime.utcnow()) def ToNanoseconds(self): """Converts Timestamp to nanoseconds since epoch.""" return self.seconds * _NANOS_PER_SECOND + self.nanos def ToMicroseconds(self): """Converts Timestamp to microseconds since epoch.""" return (self.seconds * _MICROS_PER_SECOND + self.nanos // _NANOS_PER_MICROSECOND) def ToMilliseconds(self): """Converts Timestamp to milliseconds since epoch.""" return (self.seconds * _MILLIS_PER_SECOND + self.nanos // _NANOS_PER_MILLISECOND) def ToSeconds(self): """Converts Timestamp to seconds since epoch.""" return self.seconds def FromNanoseconds(self, nanos): """Converts nanoseconds since epoch to Timestamp.""" self.seconds = nanos // _NANOS_PER_SECOND self.nanos = nanos % _NANOS_PER_SECOND def FromMicroseconds(self, micros): """Converts microseconds since epoch to Timestamp.""" self.seconds = micros // _MICROS_PER_SECOND self.nanos = (micros % _MICROS_PER_SECOND) * _NANOS_PER_MICROSECOND def FromMilliseconds(self, millis): """Converts milliseconds since epoch to Timestamp.""" self.seconds = millis // _MILLIS_PER_SECOND self.nanos = (millis % _MILLIS_PER_SECOND) * _NANOS_PER_MILLISECOND def FromSeconds(self, seconds): """Converts seconds since epoch to Timestamp.""" self.seconds = seconds self.nanos = 0 def ToDatetime(self, tzinfo=None): """Converts Timestamp to a datetime. Args: tzinfo: A datetime.tzinfo subclass; defaults to None. Returns: If tzinfo is None, returns a timezone-naive UTC datetime (with no timezone information, i.e. not aware that it's UTC). Otherwise, returns a timezone-aware datetime in the input timezone. """ delta = datetime.timedelta( seconds=self.seconds, microseconds=_RoundTowardZero(self.nanos, _NANOS_PER_MICROSECOND)) if tzinfo is None: return _EPOCH_DATETIME_NAIVE + delta else: return _EPOCH_DATETIME_AWARE.astimezone(tzinfo) + delta def FromDatetime(self, dt): """Converts datetime to Timestamp. Args: dt: A datetime. If it's timezone-naive, it's assumed to be in UTC. """ # Using this guide: http://wiki.python.org/moin/WorkingWithTime # And this conversion guide: http://docs.python.org/library/time.html # Turn the date parameter into a tuple (struct_time) that can then be # manipulated into a long value of seconds. During the conversion from # struct_time to long, the source date in UTC, and so it follows that the # correct transformation is calendar.timegm() self.seconds = calendar.timegm(dt.utctimetuple()) self.nanos = dt.microsecond * _NANOS_PER_MICROSECOND class Duration(object): """Class for Duration message type.""" __slots__ = () def ToJsonString(self): """Converts Duration to string format. Returns: A string converted from self. The string format will contains 3, 6, or 9 fractional digits depending on the precision required to represent the exact Duration value. For example: "1s", "1.010s", "1.000000100s", "-3.100s" """ _CheckDurationValid(self.seconds, self.nanos) if self.seconds < 0 or self.nanos < 0: result = '-' seconds = - self.seconds + int((0 - self.nanos) // 1e9) nanos = (0 - self.nanos) % 1e9 else: result = '' seconds = self.seconds + int(self.nanos // 1e9) nanos = self.nanos % 1e9 result += '%d' % seconds if (nanos % 1e9) == 0: # If there are 0 fractional digits, the fractional # point '.' should be omitted when serializing. return result + 's' if (nanos % 1e6) == 0: # Serialize 3 fractional digits. return result + '.%03ds' % (nanos / 1e6) if (nanos % 1e3) == 0: # Serialize 6 fractional digits. return result + '.%06ds' % (nanos / 1e3) # Serialize 9 fractional digits. return result + '.%09ds' % nanos def FromJsonString(self, value): """Converts a string to Duration. Args: value: A string to be converted. The string must end with 's'. Any fractional digits (or none) are accepted as long as they fit into precision. For example: "1s", "1.01s", "1.0000001s", "-3.100s Raises: ValueError: On parsing problems. """ if not isinstance(value, str): raise ValueError('Duration JSON value not a string: {!r}'.format(value)) if len(value) < 1 or value[-1] != 's': raise ValueError( 'Duration must end with letter "s": {0}.'.format(value)) try: pos = value.find('.') if pos == -1: seconds = int(value[:-1]) nanos = 0 else: seconds = int(value[:pos]) if value[0] == '-': nanos = int(round(float('-0{0}'.format(value[pos: -1])) *1e9)) else: nanos = int(round(float('0{0}'.format(value[pos: -1])) *1e9)) _CheckDurationValid(seconds, nanos) self.seconds = seconds self.nanos = nanos except ValueError as e: raise ValueError( 'Couldn\'t parse duration: {0} : {1}.'.format(value, e)) def ToNanoseconds(self): """Converts a Duration to nanoseconds.""" return self.seconds * _NANOS_PER_SECOND + self.nanos def ToMicroseconds(self): """Converts a Duration to microseconds.""" micros = _RoundTowardZero(self.nanos, _NANOS_PER_MICROSECOND) return self.seconds * _MICROS_PER_SECOND + micros def ToMilliseconds(self): """Converts a Duration to milliseconds.""" millis = _RoundTowardZero(self.nanos, _NANOS_PER_MILLISECOND) return self.seconds * _MILLIS_PER_SECOND + millis def ToSeconds(self): """Converts a Duration to seconds.""" return self.seconds def FromNanoseconds(self, nanos): """Converts nanoseconds to Duration.""" self._NormalizeDuration(nanos // _NANOS_PER_SECOND, nanos % _NANOS_PER_SECOND) def FromMicroseconds(self, micros): """Converts microseconds to Duration.""" self._NormalizeDuration( micros // _MICROS_PER_SECOND, (micros % _MICROS_PER_SECOND) * _NANOS_PER_MICROSECOND) def FromMilliseconds(self, millis): """Converts milliseconds to Duration.""" self._NormalizeDuration( millis // _MILLIS_PER_SECOND, (millis % _MILLIS_PER_SECOND) * _NANOS_PER_MILLISECOND) def FromSeconds(self, seconds): """Converts seconds to Duration.""" self.seconds = seconds self.nanos = 0 def ToTimedelta(self): """Converts Duration to timedelta.""" return datetime.timedelta( seconds=self.seconds, microseconds=_RoundTowardZero( self.nanos, _NANOS_PER_MICROSECOND)) def FromTimedelta(self, td): """Converts timedelta to Duration.""" self._NormalizeDuration(td.seconds + td.days * _SECONDS_PER_DAY, td.microseconds * _NANOS_PER_MICROSECOND) def _NormalizeDuration(self, seconds, nanos): """Set Duration by seconds and nanos.""" # Force nanos to be negative if the duration is negative. if seconds < 0 and nanos > 0: seconds += 1 nanos -= _NANOS_PER_SECOND self.seconds = seconds self.nanos = nanos def _CheckDurationValid(seconds, nanos): if seconds < -_DURATION_SECONDS_MAX or seconds > _DURATION_SECONDS_MAX: raise ValueError( 'Duration is not valid: Seconds {0} must be in range ' '[-315576000000, 315576000000].'.format(seconds)) if nanos <= -_NANOS_PER_SECOND or nanos >= _NANOS_PER_SECOND: raise ValueError( 'Duration is not valid: Nanos {0} must be in range ' '[-999999999, 999999999].'.format(nanos)) if (nanos < 0 and seconds > 0) or (nanos > 0 and seconds < 0): raise ValueError( 'Duration is not valid: Sign mismatch.') def _RoundTowardZero(value, divider): """Truncates the remainder part after division.""" # For some languages, the sign of the remainder is implementation # dependent if any of the operands is negative. Here we enforce # "rounded toward zero" semantics. For example, for (-5) / 2 an # implementation may give -3 as the result with the remainder being # 1. This function ensures we always return -2 (closer to zero). result = value // divider remainder = value % divider if result < 0 and remainder > 0: return result + 1 else: return result class FieldMask(object): """Class for FieldMask message type.""" __slots__ = () def ToJsonString(self): """Converts FieldMask to string according to proto3 JSON spec.""" camelcase_paths = [] for path in self.paths: camelcase_paths.append(_SnakeCaseToCamelCase(path)) return ','.join(camelcase_paths) def FromJsonString(self, value): """Converts string to FieldMask according to proto3 JSON spec.""" if not isinstance(value, str): raise ValueError('FieldMask JSON value not a string: {!r}'.format(value)) self.Clear() if value: for path in value.split(','): self.paths.append(_CamelCaseToSnakeCase(path)) def IsValidForDescriptor(self, message_descriptor): """Checks whether the FieldMask is valid for Message Descriptor.""" for path in self.paths: if not _IsValidPath(message_descriptor, path): return False return True def AllFieldsFromDescriptor(self, message_descriptor): """Gets all direct fields of Message Descriptor to FieldMask.""" self.Clear() for field in message_descriptor.fields: self.paths.append(field.name) def CanonicalFormFromMask(self, mask): """Converts a FieldMask to the canonical form. Removes paths that are covered by another path. For example, "foo.bar" is covered by "foo" and will be removed if "foo" is also in the FieldMask. Then sorts all paths in alphabetical order. Args: mask: The original FieldMask to be converted. """ tree = _FieldMaskTree(mask) tree.ToFieldMask(self) def Union(self, mask1, mask2): """Merges mask1 and mask2 into this FieldMask.""" _CheckFieldMaskMessage(mask1) _CheckFieldMaskMessage(mask2) tree = _FieldMaskTree(mask1) tree.MergeFromFieldMask(mask2) tree.ToFieldMask(self) def Intersect(self, mask1, mask2): """Intersects mask1 and mask2 into this FieldMask.""" _CheckFieldMaskMessage(mask1) _CheckFieldMaskMessage(mask2) tree = _FieldMaskTree(mask1) intersection = _FieldMaskTree() for path in mask2.paths: tree.IntersectPath(path, intersection) intersection.ToFieldMask(self) def MergeMessage( self, source, destination, replace_message_field=False, replace_repeated_field=False): """Merges fields specified in FieldMask from source to destination. Args: source: Source message. destination: The destination message to be merged into. replace_message_field: Replace message field if True. Merge message field if False. replace_repeated_field: Replace repeated field if True. Append elements of repeated field if False. """ tree = _FieldMaskTree(self) tree.MergeMessage( source, destination, replace_message_field, replace_repeated_field) def _IsValidPath(message_descriptor, path): """Checks whether the path is valid for Message Descriptor.""" parts = path.split('.') last = parts.pop() for name in parts: field = message_descriptor.fields_by_name.get(name) if (field is None or field.label == FieldDescriptor.LABEL_REPEATED or field.type != FieldDescriptor.TYPE_MESSAGE): return False message_descriptor = field.message_type return last in message_descriptor.fields_by_name def _CheckFieldMaskMessage(message): """Raises ValueError if message is not a FieldMask.""" message_descriptor = message.DESCRIPTOR if (message_descriptor.name != 'FieldMask' or message_descriptor.file.name != 'google/protobuf/field_mask.proto'): raise ValueError('Message {0} is not a FieldMask.'.format( message_descriptor.full_name)) def _SnakeCaseToCamelCase(path_name): """Converts a path name from snake_case to camelCase.""" result = [] after_underscore = False for c in path_name: if c.isupper(): raise ValueError( 'Fail to print FieldMask to Json string: Path name ' '{0} must not contain uppercase letters.'.format(path_name)) if after_underscore: if c.islower(): result.append(c.upper()) after_underscore = False else: raise ValueError( 'Fail to print FieldMask to Json string: The ' 'character after a "_" must be a lowercase letter ' 'in path name {0}.'.format(path_name)) elif c == '_': after_underscore = True else: result += c if after_underscore: raise ValueError('Fail to print FieldMask to Json string: Trailing "_" ' 'in path name {0}.'.format(path_name)) return ''.join(result) def _CamelCaseToSnakeCase(path_name): """Converts a field name from camelCase to snake_case.""" result = [] for c in path_name: if c == '_': raise ValueError('Fail to parse FieldMask: Path name ' '{0} must not contain "_"s.'.format(path_name)) if c.isupper(): result += '_' result += c.lower() else: result += c return ''.join(result) class _FieldMaskTree(object): """Represents a FieldMask in a tree structure. For example, given a FieldMask "foo.bar,foo.baz,bar.baz", the FieldMaskTree will be: [_root] -+- foo -+- bar | | | +- baz | +- bar --- baz In the tree, each leaf node represents a field path. """ __slots__ = ('_root',) def __init__(self, field_mask=None): """Initializes the tree by FieldMask.""" self._root = {} if field_mask: self.MergeFromFieldMask(field_mask) def MergeFromFieldMask(self, field_mask): """Merges a FieldMask to the tree.""" for path in field_mask.paths: self.AddPath(path) def AddPath(self, path): """Adds a field path into the tree. If the field path to add is a sub-path of an existing field path in the tree (i.e., a leaf node), it means the tree already matches the given path so nothing will be added to the tree. If the path matches an existing non-leaf node in the tree, that non-leaf node will be turned into a leaf node with all its children removed because the path matches all the node's children. Otherwise, a new path will be added. Args: path: The field path to add. """ node = self._root for name in path.split('.'): if name not in node: node[name] = {} elif not node[name]: # Pre-existing empty node implies we already have this entire tree. return node = node[name] # Remove any sub-trees we might have had. node.clear() def ToFieldMask(self, field_mask): """Converts the tree to a FieldMask.""" field_mask.Clear() _AddFieldPaths(self._root, '', field_mask) def IntersectPath(self, path, intersection): """Calculates the intersection part of a field path with this tree. Args: path: The field path to calculates. intersection: The out tree to record the intersection part. """ node = self._root for name in path.split('.'): if name not in node: return elif not node[name]: intersection.AddPath(path) return node = node[name] intersection.AddLeafNodes(path, node) def AddLeafNodes(self, prefix, node): """Adds leaf nodes begin with prefix to this tree.""" if not node: self.AddPath(prefix) for name in node: child_path = prefix + '.' + name self.AddLeafNodes(child_path, node[name]) def MergeMessage( self, source, destination, replace_message, replace_repeated): """Merge all fields specified by this tree from source to destination.""" _MergeMessage( self._root, source, destination, replace_message, replace_repeated) def _StrConvert(value): """Converts value to str if it is not.""" # This file is imported by c extension and some methods like ClearField # requires string for the field name. py2/py3 has different text # type and may use unicode. if not isinstance(value, str): return value.encode('utf-8') return value def _MergeMessage( node, source, destination, replace_message, replace_repeated): """Merge all fields specified by a sub-tree from source to destination.""" source_descriptor = source.DESCRIPTOR for name in node: child = node[name] field = source_descriptor.fields_by_name[name] if field is None: raise ValueError('Error: Can\'t find field {0} in message {1}.'.format( name, source_descriptor.full_name)) if child: # Sub-paths are only allowed for singular message fields. if (field.label == FieldDescriptor.LABEL_REPEATED or field.cpp_type != FieldDescriptor.CPPTYPE_MESSAGE): raise ValueError('Error: Field {0} in message {1} is not a singular ' 'message field and cannot have sub-fields.'.format( name, source_descriptor.full_name)) if source.HasField(name): _MergeMessage( child, getattr(source, name), getattr(destination, name), replace_message, replace_repeated) continue if field.label == FieldDescriptor.LABEL_REPEATED: if replace_repeated: destination.ClearField(_StrConvert(name)) repeated_source = getattr(source, name) repeated_destination = getattr(destination, name) repeated_destination.MergeFrom(repeated_source) else: if field.cpp_type == FieldDescriptor.CPPTYPE_MESSAGE: if replace_message: destination.ClearField(_StrConvert(name)) if source.HasField(name): getattr(destination, name).MergeFrom(getattr(source, name)) else: setattr(destination, name, getattr(source, name)) def _AddFieldPaths(node, prefix, field_mask): """Adds the field paths descended from node to field_mask.""" if not node and prefix: field_mask.paths.append(prefix) return for name in sorted(node): if prefix: child_path = prefix + '.' + name else: child_path = name _AddFieldPaths(node[name], child_path, field_mask) def _SetStructValue(struct_value, value): if value is None: struct_value.null_value = 0 elif isinstance(value, bool): # Note: this check must come before the number check because in Python # True and False are also considered numbers. struct_value.bool_value = value elif isinstance(value, str): struct_value.string_value = value elif isinstance(value, (int, float)): struct_value.number_value = value elif isinstance(value, (dict, Struct)): struct_value.struct_value.Clear() struct_value.struct_value.update(value) elif isinstance(value, (list, ListValue)): struct_value.list_value.Clear() struct_value.list_value.extend(value) else: raise ValueError('Unexpected type') def _GetStructValue(struct_value): which = struct_value.WhichOneof('kind') if which == 'struct_value': return struct_value.struct_value elif which == 'null_value': return None elif which == 'number_value': return struct_value.number_value elif which == 'string_value': return struct_value.string_value elif which == 'bool_value': return struct_value.bool_value elif which == 'list_value': return struct_value.list_value elif which is None: raise ValueError('Value not set') class Struct(object): """Class for Struct message type.""" __slots__ = () def __getitem__(self, key): return _GetStructValue(self.fields[key]) def __contains__(self, item): return item in self.fields def __setitem__(self, key, value): _SetStructValue(self.fields[key], value) def __delitem__(self, key): del self.fields[key] def __len__(self): return len(self.fields) def __iter__(self): return iter(self.fields) def keys(self): # pylint: disable=invalid-name return self.fields.keys() def values(self): # pylint: disable=invalid-name return [self[key] for key in self] def items(self): # pylint: disable=invalid-name return [(key, self[key]) for key in self] def get_or_create_list(self, key): """Returns a list for this key, creating if it didn't exist already.""" if not self.fields[key].HasField('list_value'): # Clear will mark list_value modified which will indeed create a list. self.fields[key].list_value.Clear() return self.fields[key].list_value def get_or_create_struct(self, key): """Returns a struct for this key, creating if it didn't exist already.""" if not self.fields[key].HasField('struct_value'): # Clear will mark struct_value modified which will indeed create a struct. self.fields[key].struct_value.Clear() return self.fields[key].struct_value def update(self, dictionary): # pylint: disable=invalid-name for key, value in dictionary.items(): _SetStructValue(self.fields[key], value) collections.abc.MutableMapping.register(Struct) class ListValue(object): """Class for ListValue message type.""" __slots__ = () def __len__(self): return len(self.values) def append(self, value): _SetStructValue(self.values.add(), value) def extend(self, elem_seq): for value in elem_seq: self.append(value) def __getitem__(self, index): """Retrieves item by the specified index.""" return _GetStructValue(self.values.__getitem__(index)) def __setitem__(self, index, value): _SetStructValue(self.values.__getitem__(index), value) def __delitem__(self, key): del self.values[key] def items(self): for i in range(len(self)): yield self[i] def add_struct(self): """Appends and returns a struct value as the next value in the list.""" struct_value = self.values.add().struct_value # Clear will mark struct_value modified which will indeed create a struct. struct_value.Clear() return struct_value def add_list(self): """Appends and returns a list value as the next value in the list.""" list_value = self.values.add().list_value # Clear will mark list_value modified which will indeed create a list. list_value.Clear() return list_value collections.abc.MutableSequence.register(ListValue) WKTBASES = { 'google.protobuf.Any': Any, 'google.protobuf.Duration': Duration, 'google.protobuf.FieldMask': FieldMask, 'google.protobuf.ListValue': ListValue, 'google.protobuf.Struct': Struct, 'google.protobuf.Timestamp': Timestamp, } ================================================ FILE: openpype/hosts/hiero/vendor/google/protobuf/internal/wire_format.py ================================================ # Protocol Buffers - Google's data interchange format # Copyright 2008 Google Inc. All rights reserved. # https://developers.google.com/protocol-buffers/ # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are # met: # # * Redistributions of source code must retain the above copyright # notice, this list of conditions and the following disclaimer. # * Redistributions in binary form must reproduce the above # copyright notice, this list of conditions and the following disclaimer # in the documentation and/or other materials provided with the # distribution. # * Neither the name of Google Inc. nor the names of its # contributors may be used to endorse or promote products derived from # this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. """Constants and static functions to support protocol buffer wire format.""" __author__ = 'robinson@google.com (Will Robinson)' import struct from google.protobuf import descriptor from google.protobuf import message TAG_TYPE_BITS = 3 # Number of bits used to hold type info in a proto tag. TAG_TYPE_MASK = (1 << TAG_TYPE_BITS) - 1 # 0x7 # These numbers identify the wire type of a protocol buffer value. # We use the least-significant TAG_TYPE_BITS bits of the varint-encoded # tag-and-type to store one of these WIRETYPE_* constants. # These values must match WireType enum in google/protobuf/wire_format.h. WIRETYPE_VARINT = 0 WIRETYPE_FIXED64 = 1 WIRETYPE_LENGTH_DELIMITED = 2 WIRETYPE_START_GROUP = 3 WIRETYPE_END_GROUP = 4 WIRETYPE_FIXED32 = 5 _WIRETYPE_MAX = 5 # Bounds for various integer types. INT32_MAX = int((1 << 31) - 1) INT32_MIN = int(-(1 << 31)) UINT32_MAX = (1 << 32) - 1 INT64_MAX = (1 << 63) - 1 INT64_MIN = -(1 << 63) UINT64_MAX = (1 << 64) - 1 # "struct" format strings that will encode/decode the specified formats. FORMAT_UINT32_LITTLE_ENDIAN = '> TAG_TYPE_BITS), (tag & TAG_TYPE_MASK) def ZigZagEncode(value): """ZigZag Transform: Encodes signed integers so that they can be effectively used with varint encoding. See wire_format.h for more details. """ if value >= 0: return value << 1 return (value << 1) ^ (~0) def ZigZagDecode(value): """Inverse of ZigZagEncode().""" if not value & 0x1: return value >> 1 return (value >> 1) ^ (~0) # The *ByteSize() functions below return the number of bytes required to # serialize "field number + type" information and then serialize the value. def Int32ByteSize(field_number, int32): return Int64ByteSize(field_number, int32) def Int32ByteSizeNoTag(int32): return _VarUInt64ByteSizeNoTag(0xffffffffffffffff & int32) def Int64ByteSize(field_number, int64): # Have to convert to uint before calling UInt64ByteSize(). return UInt64ByteSize(field_number, 0xffffffffffffffff & int64) def UInt32ByteSize(field_number, uint32): return UInt64ByteSize(field_number, uint32) def UInt64ByteSize(field_number, uint64): return TagByteSize(field_number) + _VarUInt64ByteSizeNoTag(uint64) def SInt32ByteSize(field_number, int32): return UInt32ByteSize(field_number, ZigZagEncode(int32)) def SInt64ByteSize(field_number, int64): return UInt64ByteSize(field_number, ZigZagEncode(int64)) def Fixed32ByteSize(field_number, fixed32): return TagByteSize(field_number) + 4 def Fixed64ByteSize(field_number, fixed64): return TagByteSize(field_number) + 8 def SFixed32ByteSize(field_number, sfixed32): return TagByteSize(field_number) + 4 def SFixed64ByteSize(field_number, sfixed64): return TagByteSize(field_number) + 8 def FloatByteSize(field_number, flt): return TagByteSize(field_number) + 4 def DoubleByteSize(field_number, double): return TagByteSize(field_number) + 8 def BoolByteSize(field_number, b): return TagByteSize(field_number) + 1 def EnumByteSize(field_number, enum): return UInt32ByteSize(field_number, enum) def StringByteSize(field_number, string): return BytesByteSize(field_number, string.encode('utf-8')) def BytesByteSize(field_number, b): return (TagByteSize(field_number) + _VarUInt64ByteSizeNoTag(len(b)) + len(b)) def GroupByteSize(field_number, message): return (2 * TagByteSize(field_number) # START and END group. + message.ByteSize()) def MessageByteSize(field_number, message): return (TagByteSize(field_number) + _VarUInt64ByteSizeNoTag(message.ByteSize()) + message.ByteSize()) def MessageSetItemByteSize(field_number, msg): # First compute the sizes of the tags. # There are 2 tags for the beginning and ending of the repeated group, that # is field number 1, one with field number 2 (type_id) and one with field # number 3 (message). total_size = (2 * TagByteSize(1) + TagByteSize(2) + TagByteSize(3)) # Add the number of bytes for type_id. total_size += _VarUInt64ByteSizeNoTag(field_number) message_size = msg.ByteSize() # The number of bytes for encoding the length of the message. total_size += _VarUInt64ByteSizeNoTag(message_size) # The size of the message. total_size += message_size return total_size def TagByteSize(field_number): """Returns the bytes required to serialize a tag with this field number.""" # Just pass in type 0, since the type won't affect the tag+type size. return _VarUInt64ByteSizeNoTag(PackTag(field_number, 0)) # Private helper function for the *ByteSize() functions above. def _VarUInt64ByteSizeNoTag(uint64): """Returns the number of bytes required to serialize a single varint using boundary value comparisons. (unrolled loop optimization -WPierce) uint64 must be unsigned. """ if uint64 <= 0x7f: return 1 if uint64 <= 0x3fff: return 2 if uint64 <= 0x1fffff: return 3 if uint64 <= 0xfffffff: return 4 if uint64 <= 0x7ffffffff: return 5 if uint64 <= 0x3ffffffffff: return 6 if uint64 <= 0x1ffffffffffff: return 7 if uint64 <= 0xffffffffffffff: return 8 if uint64 <= 0x7fffffffffffffff: return 9 if uint64 > UINT64_MAX: raise message.EncodeError('Value out of range: %d' % uint64) return 10 NON_PACKABLE_TYPES = ( descriptor.FieldDescriptor.TYPE_STRING, descriptor.FieldDescriptor.TYPE_GROUP, descriptor.FieldDescriptor.TYPE_MESSAGE, descriptor.FieldDescriptor.TYPE_BYTES ) def IsTypePackable(field_type): """Return true iff packable = true is valid for fields of this type. Args: field_type: a FieldDescriptor::Type value. Returns: True iff fields of this type are packable. """ return field_type not in NON_PACKABLE_TYPES ================================================ FILE: openpype/hosts/hiero/vendor/google/protobuf/json_format.py ================================================ # Protocol Buffers - Google's data interchange format # Copyright 2008 Google Inc. All rights reserved. # https://developers.google.com/protocol-buffers/ # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are # met: # # * Redistributions of source code must retain the above copyright # notice, this list of conditions and the following disclaimer. # * Redistributions in binary form must reproduce the above # copyright notice, this list of conditions and the following disclaimer # in the documentation and/or other materials provided with the # distribution. # * Neither the name of Google Inc. nor the names of its # contributors may be used to endorse or promote products derived from # this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. """Contains routines for printing protocol messages in JSON format. Simple usage example: # Create a proto object and serialize it to a json format string. message = my_proto_pb2.MyMessage(foo='bar') json_string = json_format.MessageToJson(message) # Parse a json format string to proto object. message = json_format.Parse(json_string, my_proto_pb2.MyMessage()) """ __author__ = 'jieluo@google.com (Jie Luo)' import base64 from collections import OrderedDict import json import math from operator import methodcaller import re import sys from google.protobuf.internal import type_checkers from google.protobuf import descriptor from google.protobuf import symbol_database _TIMESTAMPFOMAT = '%Y-%m-%dT%H:%M:%S' _INT_TYPES = frozenset([descriptor.FieldDescriptor.CPPTYPE_INT32, descriptor.FieldDescriptor.CPPTYPE_UINT32, descriptor.FieldDescriptor.CPPTYPE_INT64, descriptor.FieldDescriptor.CPPTYPE_UINT64]) _INT64_TYPES = frozenset([descriptor.FieldDescriptor.CPPTYPE_INT64, descriptor.FieldDescriptor.CPPTYPE_UINT64]) _FLOAT_TYPES = frozenset([descriptor.FieldDescriptor.CPPTYPE_FLOAT, descriptor.FieldDescriptor.CPPTYPE_DOUBLE]) _INFINITY = 'Infinity' _NEG_INFINITY = '-Infinity' _NAN = 'NaN' _UNPAIRED_SURROGATE_PATTERN = re.compile( u'[\ud800-\udbff](?![\udc00-\udfff])|(? self.max_recursion_depth: raise ParseError('Message too deep. Max recursion depth is {0}'.format( self.max_recursion_depth)) message_descriptor = message.DESCRIPTOR full_name = message_descriptor.full_name if not path: path = message_descriptor.name if _IsWrapperMessage(message_descriptor): self._ConvertWrapperMessage(value, message, path) elif full_name in _WKTJSONMETHODS: methodcaller(_WKTJSONMETHODS[full_name][1], value, message, path)(self) else: self._ConvertFieldValuePair(value, message, path) self.recursion_depth -= 1 def _ConvertFieldValuePair(self, js, message, path): """Convert field value pairs into regular message. Args: js: A JSON object to convert the field value pairs. message: A regular protocol message to record the data. path: parent path to log parse error info. Raises: ParseError: In case of problems converting. """ names = [] message_descriptor = message.DESCRIPTOR fields_by_json_name = dict((f.json_name, f) for f in message_descriptor.fields) for name in js: try: field = fields_by_json_name.get(name, None) if not field: field = message_descriptor.fields_by_name.get(name, None) if not field and _VALID_EXTENSION_NAME.match(name): if not message_descriptor.is_extendable: raise ParseError( 'Message type {0} does not have extensions at {1}'.format( message_descriptor.full_name, path)) identifier = name[1:-1] # strip [] brackets # pylint: disable=protected-access field = message.Extensions._FindExtensionByName(identifier) # pylint: enable=protected-access if not field: # Try looking for extension by the message type name, dropping the # field name following the final . separator in full_name. identifier = '.'.join(identifier.split('.')[:-1]) # pylint: disable=protected-access field = message.Extensions._FindExtensionByName(identifier) # pylint: enable=protected-access if not field: if self.ignore_unknown_fields: continue raise ParseError( ('Message type "{0}" has no field named "{1}" at "{2}".\n' ' Available Fields(except extensions): "{3}"').format( message_descriptor.full_name, name, path, [f.json_name for f in message_descriptor.fields])) if name in names: raise ParseError('Message type "{0}" should not have multiple ' '"{1}" fields at "{2}".'.format( message.DESCRIPTOR.full_name, name, path)) names.append(name) value = js[name] # Check no other oneof field is parsed. if field.containing_oneof is not None and value is not None: oneof_name = field.containing_oneof.name if oneof_name in names: raise ParseError('Message type "{0}" should not have multiple ' '"{1}" oneof fields at "{2}".'.format( message.DESCRIPTOR.full_name, oneof_name, path)) names.append(oneof_name) if value is None: if (field.cpp_type == descriptor.FieldDescriptor.CPPTYPE_MESSAGE and field.message_type.full_name == 'google.protobuf.Value'): sub_message = getattr(message, field.name) sub_message.null_value = 0 elif (field.cpp_type == descriptor.FieldDescriptor.CPPTYPE_ENUM and field.enum_type.full_name == 'google.protobuf.NullValue'): setattr(message, field.name, 0) else: message.ClearField(field.name) continue # Parse field value. if _IsMapEntry(field): message.ClearField(field.name) self._ConvertMapFieldValue(value, message, field, '{0}.{1}'.format(path, name)) elif field.label == descriptor.FieldDescriptor.LABEL_REPEATED: message.ClearField(field.name) if not isinstance(value, list): raise ParseError('repeated field {0} must be in [] which is ' '{1} at {2}'.format(name, value, path)) if field.cpp_type == descriptor.FieldDescriptor.CPPTYPE_MESSAGE: # Repeated message field. for index, item in enumerate(value): sub_message = getattr(message, field.name).add() # None is a null_value in Value. if (item is None and sub_message.DESCRIPTOR.full_name != 'google.protobuf.Value'): raise ParseError('null is not allowed to be used as an element' ' in a repeated field at {0}.{1}[{2}]'.format( path, name, index)) self.ConvertMessage(item, sub_message, '{0}.{1}[{2}]'.format(path, name, index)) else: # Repeated scalar field. for index, item in enumerate(value): if item is None: raise ParseError('null is not allowed to be used as an element' ' in a repeated field at {0}.{1}[{2}]'.format( path, name, index)) getattr(message, field.name).append( _ConvertScalarFieldValue( item, field, '{0}.{1}[{2}]'.format(path, name, index))) elif field.cpp_type == descriptor.FieldDescriptor.CPPTYPE_MESSAGE: if field.is_extension: sub_message = message.Extensions[field] else: sub_message = getattr(message, field.name) sub_message.SetInParent() self.ConvertMessage(value, sub_message, '{0}.{1}'.format(path, name)) else: if field.is_extension: message.Extensions[field] = _ConvertScalarFieldValue( value, field, '{0}.{1}'.format(path, name)) else: setattr( message, field.name, _ConvertScalarFieldValue(value, field, '{0}.{1}'.format(path, name))) except ParseError as e: if field and field.containing_oneof is None: raise ParseError('Failed to parse {0} field: {1}.'.format(name, e)) else: raise ParseError(str(e)) except ValueError as e: raise ParseError('Failed to parse {0} field: {1}.'.format(name, e)) except TypeError as e: raise ParseError('Failed to parse {0} field: {1}.'.format(name, e)) def _ConvertAnyMessage(self, value, message, path): """Convert a JSON representation into Any message.""" if isinstance(value, dict) and not value: return try: type_url = value['@type'] except KeyError: raise ParseError( '@type is missing when parsing any message at {0}'.format(path)) try: sub_message = _CreateMessageFromTypeUrl(type_url, self.descriptor_pool) except TypeError as e: raise ParseError('{0} at {1}'.format(e, path)) message_descriptor = sub_message.DESCRIPTOR full_name = message_descriptor.full_name if _IsWrapperMessage(message_descriptor): self._ConvertWrapperMessage(value['value'], sub_message, '{0}.value'.format(path)) elif full_name in _WKTJSONMETHODS: methodcaller(_WKTJSONMETHODS[full_name][1], value['value'], sub_message, '{0}.value'.format(path))( self) else: del value['@type'] self._ConvertFieldValuePair(value, sub_message, path) value['@type'] = type_url # Sets Any message message.value = sub_message.SerializeToString() message.type_url = type_url def _ConvertGenericMessage(self, value, message, path): """Convert a JSON representation into message with FromJsonString.""" # Duration, Timestamp, FieldMask have a FromJsonString method to do the # conversion. Users can also call the method directly. try: message.FromJsonString(value) except ValueError as e: raise ParseError('{0} at {1}'.format(e, path)) def _ConvertValueMessage(self, value, message, path): """Convert a JSON representation into Value message.""" if isinstance(value, dict): self._ConvertStructMessage(value, message.struct_value, path) elif isinstance(value, list): self._ConvertListValueMessage(value, message.list_value, path) elif value is None: message.null_value = 0 elif isinstance(value, bool): message.bool_value = value elif isinstance(value, str): message.string_value = value elif isinstance(value, _INT_OR_FLOAT): message.number_value = value else: raise ParseError('Value {0} has unexpected type {1} at {2}'.format( value, type(value), path)) def _ConvertListValueMessage(self, value, message, path): """Convert a JSON representation into ListValue message.""" if not isinstance(value, list): raise ParseError('ListValue must be in [] which is {0} at {1}'.format( value, path)) message.ClearField('values') for index, item in enumerate(value): self._ConvertValueMessage(item, message.values.add(), '{0}[{1}]'.format(path, index)) def _ConvertStructMessage(self, value, message, path): """Convert a JSON representation into Struct message.""" if not isinstance(value, dict): raise ParseError('Struct must be in a dict which is {0} at {1}'.format( value, path)) # Clear will mark the struct as modified so it will be created even if # there are no values. message.Clear() for key in value: self._ConvertValueMessage(value[key], message.fields[key], '{0}.{1}'.format(path, key)) return def _ConvertWrapperMessage(self, value, message, path): """Convert a JSON representation into Wrapper message.""" field = message.DESCRIPTOR.fields_by_name['value'] setattr( message, 'value', _ConvertScalarFieldValue(value, field, path='{0}.value'.format(path))) def _ConvertMapFieldValue(self, value, message, field, path): """Convert map field value for a message map field. Args: value: A JSON object to convert the map field value. message: A protocol message to record the converted data. field: The descriptor of the map field to be converted. path: parent path to log parse error info. Raises: ParseError: In case of convert problems. """ if not isinstance(value, dict): raise ParseError( 'Map field {0} must be in a dict which is {1} at {2}'.format( field.name, value, path)) key_field = field.message_type.fields_by_name['key'] value_field = field.message_type.fields_by_name['value'] for key in value: key_value = _ConvertScalarFieldValue(key, key_field, '{0}.key'.format(path), True) if value_field.cpp_type == descriptor.FieldDescriptor.CPPTYPE_MESSAGE: self.ConvertMessage(value[key], getattr(message, field.name)[key_value], '{0}[{1}]'.format(path, key_value)) else: getattr(message, field.name)[key_value] = _ConvertScalarFieldValue( value[key], value_field, path='{0}[{1}]'.format(path, key_value)) def _ConvertScalarFieldValue(value, field, path, require_str=False): """Convert a single scalar field value. Args: value: A scalar value to convert the scalar field value. field: The descriptor of the field to convert. path: parent path to log parse error info. require_str: If True, the field value must be a str. Returns: The converted scalar field value Raises: ParseError: In case of convert problems. """ try: if field.cpp_type in _INT_TYPES: return _ConvertInteger(value) elif field.cpp_type in _FLOAT_TYPES: return _ConvertFloat(value, field) elif field.cpp_type == descriptor.FieldDescriptor.CPPTYPE_BOOL: return _ConvertBool(value, require_str) elif field.cpp_type == descriptor.FieldDescriptor.CPPTYPE_STRING: if field.type == descriptor.FieldDescriptor.TYPE_BYTES: if isinstance(value, str): encoded = value.encode('utf-8') else: encoded = value # Add extra padding '=' padded_value = encoded + b'=' * (4 - len(encoded) % 4) return base64.urlsafe_b64decode(padded_value) else: # Checking for unpaired surrogates appears to be unreliable, # depending on the specific Python version, so we check manually. if _UNPAIRED_SURROGATE_PATTERN.search(value): raise ParseError('Unpaired surrogate') return value elif field.cpp_type == descriptor.FieldDescriptor.CPPTYPE_ENUM: # Convert an enum value. enum_value = field.enum_type.values_by_name.get(value, None) if enum_value is None: try: number = int(value) enum_value = field.enum_type.values_by_number.get(number, None) except ValueError: raise ParseError('Invalid enum value {0} for enum type {1}'.format( value, field.enum_type.full_name)) if enum_value is None: if field.file.syntax == 'proto3': # Proto3 accepts unknown enums. return number raise ParseError('Invalid enum value {0} for enum type {1}'.format( value, field.enum_type.full_name)) return enum_value.number except ParseError as e: raise ParseError('{0} at {1}'.format(e, path)) def _ConvertInteger(value): """Convert an integer. Args: value: A scalar value to convert. Returns: The integer value. Raises: ParseError: If an integer couldn't be consumed. """ if isinstance(value, float) and not value.is_integer(): raise ParseError('Couldn\'t parse integer: {0}'.format(value)) if isinstance(value, str) and value.find(' ') != -1: raise ParseError('Couldn\'t parse integer: "{0}"'.format(value)) if isinstance(value, bool): raise ParseError('Bool value {0} is not acceptable for ' 'integer field'.format(value)) return int(value) def _ConvertFloat(value, field): """Convert an floating point number.""" if isinstance(value, float): if math.isnan(value): raise ParseError('Couldn\'t parse NaN, use quoted "NaN" instead') if math.isinf(value): if value > 0: raise ParseError('Couldn\'t parse Infinity or value too large, ' 'use quoted "Infinity" instead') else: raise ParseError('Couldn\'t parse -Infinity or value too small, ' 'use quoted "-Infinity" instead') if field.cpp_type == descriptor.FieldDescriptor.CPPTYPE_FLOAT: # pylint: disable=protected-access if value > type_checkers._FLOAT_MAX: raise ParseError('Float value too large') # pylint: disable=protected-access if value < type_checkers._FLOAT_MIN: raise ParseError('Float value too small') if value == 'nan': raise ParseError('Couldn\'t parse float "nan", use "NaN" instead') try: # Assume Python compatible syntax. return float(value) except ValueError: # Check alternative spellings. if value == _NEG_INFINITY: return float('-inf') elif value == _INFINITY: return float('inf') elif value == _NAN: return float('nan') else: raise ParseError('Couldn\'t parse float: {0}'.format(value)) def _ConvertBool(value, require_str): """Convert a boolean value. Args: value: A scalar value to convert. require_str: If True, value must be a str. Returns: The bool parsed. Raises: ParseError: If a boolean value couldn't be consumed. """ if require_str: if value == 'true': return True elif value == 'false': return False else: raise ParseError('Expected "true" or "false", not {0}'.format(value)) if not isinstance(value, bool): raise ParseError('Expected true or false without quotes') return value _WKTJSONMETHODS = { 'google.protobuf.Any': ['_AnyMessageToJsonObject', '_ConvertAnyMessage'], 'google.protobuf.Duration': ['_GenericMessageToJsonObject', '_ConvertGenericMessage'], 'google.protobuf.FieldMask': ['_GenericMessageToJsonObject', '_ConvertGenericMessage'], 'google.protobuf.ListValue': ['_ListValueMessageToJsonObject', '_ConvertListValueMessage'], 'google.protobuf.Struct': ['_StructMessageToJsonObject', '_ConvertStructMessage'], 'google.protobuf.Timestamp': ['_GenericMessageToJsonObject', '_ConvertGenericMessage'], 'google.protobuf.Value': ['_ValueMessageToJsonObject', '_ConvertValueMessage'] } ================================================ FILE: openpype/hosts/hiero/vendor/google/protobuf/message.py ================================================ # Protocol Buffers - Google's data interchange format # Copyright 2008 Google Inc. All rights reserved. # https://developers.google.com/protocol-buffers/ # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are # met: # # * Redistributions of source code must retain the above copyright # notice, this list of conditions and the following disclaimer. # * Redistributions in binary form must reproduce the above # copyright notice, this list of conditions and the following disclaimer # in the documentation and/or other materials provided with the # distribution. # * Neither the name of Google Inc. nor the names of its # contributors may be used to endorse or promote products derived from # this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. # TODO(robinson): We should just make these methods all "pure-virtual" and move # all implementation out, into reflection.py for now. """Contains an abstract base class for protocol messages.""" __author__ = 'robinson@google.com (Will Robinson)' class Error(Exception): """Base error type for this module.""" pass class DecodeError(Error): """Exception raised when deserializing messages.""" pass class EncodeError(Error): """Exception raised when serializing messages.""" pass class Message(object): """Abstract base class for protocol messages. Protocol message classes are almost always generated by the protocol compiler. These generated types subclass Message and implement the methods shown below. """ # TODO(robinson): Link to an HTML document here. # TODO(robinson): Document that instances of this class will also # have an Extensions attribute with __getitem__ and __setitem__. # Again, not sure how to best convey this. # TODO(robinson): Document that the class must also have a static # RegisterExtension(extension_field) method. # Not sure how to best express at this point. # TODO(robinson): Document these fields and methods. __slots__ = [] #: The :class:`google.protobuf.descriptor.Descriptor` for this message type. DESCRIPTOR = None def __deepcopy__(self, memo=None): clone = type(self)() clone.MergeFrom(self) return clone def __eq__(self, other_msg): """Recursively compares two messages by value and structure.""" raise NotImplementedError def __ne__(self, other_msg): # Can't just say self != other_msg, since that would infinitely recurse. :) return not self == other_msg def __hash__(self): raise TypeError('unhashable object') def __str__(self): """Outputs a human-readable representation of the message.""" raise NotImplementedError def __unicode__(self): """Outputs a human-readable representation of the message.""" raise NotImplementedError def MergeFrom(self, other_msg): """Merges the contents of the specified message into current message. This method merges the contents of the specified message into the current message. Singular fields that are set in the specified message overwrite the corresponding fields in the current message. Repeated fields are appended. Singular sub-messages and groups are recursively merged. Args: other_msg (Message): A message to merge into the current message. """ raise NotImplementedError def CopyFrom(self, other_msg): """Copies the content of the specified message into the current message. The method clears the current message and then merges the specified message using MergeFrom. Args: other_msg (Message): A message to copy into the current one. """ if self is other_msg: return self.Clear() self.MergeFrom(other_msg) def Clear(self): """Clears all data that was set in the message.""" raise NotImplementedError def SetInParent(self): """Mark this as present in the parent. This normally happens automatically when you assign a field of a sub-message, but sometimes you want to make the sub-message present while keeping it empty. If you find yourself using this, you may want to reconsider your design. """ raise NotImplementedError def IsInitialized(self): """Checks if the message is initialized. Returns: bool: The method returns True if the message is initialized (i.e. all of its required fields are set). """ raise NotImplementedError # TODO(robinson): MergeFromString() should probably return None and be # implemented in terms of a helper that returns the # of bytes read. Our # deserialization routines would use the helper when recursively # deserializing, but the end user would almost always just want the no-return # MergeFromString(). def MergeFromString(self, serialized): """Merges serialized protocol buffer data into this message. When we find a field in `serialized` that is already present in this message: - If it's a "repeated" field, we append to the end of our list. - Else, if it's a scalar, we overwrite our field. - Else, (it's a nonrepeated composite), we recursively merge into the existing composite. Args: serialized (bytes): Any object that allows us to call ``memoryview(serialized)`` to access a string of bytes using the buffer interface. Returns: int: The number of bytes read from `serialized`. For non-group messages, this will always be `len(serialized)`, but for messages which are actually groups, this will generally be less than `len(serialized)`, since we must stop when we reach an ``END_GROUP`` tag. Note that if we *do* stop because of an ``END_GROUP`` tag, the number of bytes returned does not include the bytes for the ``END_GROUP`` tag information. Raises: DecodeError: if the input cannot be parsed. """ # TODO(robinson): Document handling of unknown fields. # TODO(robinson): When we switch to a helper, this will return None. raise NotImplementedError def ParseFromString(self, serialized): """Parse serialized protocol buffer data into this message. Like :func:`MergeFromString()`, except we clear the object first. Raises: message.DecodeError if the input cannot be parsed. """ self.Clear() return self.MergeFromString(serialized) def SerializeToString(self, **kwargs): """Serializes the protocol message to a binary string. Keyword Args: deterministic (bool): If true, requests deterministic serialization of the protobuf, with predictable ordering of map keys. Returns: A binary string representation of the message if all of the required fields in the message are set (i.e. the message is initialized). Raises: EncodeError: if the message isn't initialized (see :func:`IsInitialized`). """ raise NotImplementedError def SerializePartialToString(self, **kwargs): """Serializes the protocol message to a binary string. This method is similar to SerializeToString but doesn't check if the message is initialized. Keyword Args: deterministic (bool): If true, requests deterministic serialization of the protobuf, with predictable ordering of map keys. Returns: bytes: A serialized representation of the partial message. """ raise NotImplementedError # TODO(robinson): Decide whether we like these better # than auto-generated has_foo() and clear_foo() methods # on the instances themselves. This way is less consistent # with C++, but it makes reflection-type access easier and # reduces the number of magically autogenerated things. # # TODO(robinson): Be sure to document (and test) exactly # which field names are accepted here. Are we case-sensitive? # What do we do with fields that share names with Python keywords # like 'lambda' and 'yield'? # # nnorwitz says: # """ # Typically (in python), an underscore is appended to names that are # keywords. So they would become lambda_ or yield_. # """ def ListFields(self): """Returns a list of (FieldDescriptor, value) tuples for present fields. A message field is non-empty if HasField() would return true. A singular primitive field is non-empty if HasField() would return true in proto2 or it is non zero in proto3. A repeated field is non-empty if it contains at least one element. The fields are ordered by field number. Returns: list[tuple(FieldDescriptor, value)]: field descriptors and values for all fields in the message which are not empty. The values vary by field type. """ raise NotImplementedError def HasField(self, field_name): """Checks if a certain field is set for the message. For a oneof group, checks if any field inside is set. Note that if the field_name is not defined in the message descriptor, :exc:`ValueError` will be raised. Args: field_name (str): The name of the field to check for presence. Returns: bool: Whether a value has been set for the named field. Raises: ValueError: if the `field_name` is not a member of this message. """ raise NotImplementedError def ClearField(self, field_name): """Clears the contents of a given field. Inside a oneof group, clears the field set. If the name neither refers to a defined field or oneof group, :exc:`ValueError` is raised. Args: field_name (str): The name of the field to check for presence. Raises: ValueError: if the `field_name` is not a member of this message. """ raise NotImplementedError def WhichOneof(self, oneof_group): """Returns the name of the field that is set inside a oneof group. If no field is set, returns None. Args: oneof_group (str): the name of the oneof group to check. Returns: str or None: The name of the group that is set, or None. Raises: ValueError: no group with the given name exists """ raise NotImplementedError def HasExtension(self, extension_handle): """Checks if a certain extension is present for this message. Extensions are retrieved using the :attr:`Extensions` mapping (if present). Args: extension_handle: The handle for the extension to check. Returns: bool: Whether the extension is present for this message. Raises: KeyError: if the extension is repeated. Similar to repeated fields, there is no separate notion of presence: a "not present" repeated extension is an empty list. """ raise NotImplementedError def ClearExtension(self, extension_handle): """Clears the contents of a given extension. Args: extension_handle: The handle for the extension to clear. """ raise NotImplementedError def UnknownFields(self): """Returns the UnknownFieldSet. Returns: UnknownFieldSet: The unknown fields stored in this message. """ raise NotImplementedError def DiscardUnknownFields(self): """Clears all fields in the :class:`UnknownFieldSet`. This operation is recursive for nested message. """ raise NotImplementedError def ByteSize(self): """Returns the serialized size of this message. Recursively calls ByteSize() on all contained messages. Returns: int: The number of bytes required to serialize this message. """ raise NotImplementedError @classmethod def FromString(cls, s): raise NotImplementedError @staticmethod def RegisterExtension(extension_handle): raise NotImplementedError def _SetListener(self, message_listener): """Internal method used by the protocol message implementation. Clients should not call this directly. Sets a listener that this message will call on certain state transitions. The purpose of this method is to register back-edges from children to parents at runtime, for the purpose of setting "has" bits and byte-size-dirty bits in the parent and ancestor objects whenever a child or descendant object is modified. If the client wants to disconnect this Message from the object tree, she explicitly sets callback to None. If message_listener is None, unregisters any existing listener. Otherwise, message_listener must implement the MessageListener interface in internal/message_listener.py, and we discard any listener registered via a previous _SetListener() call. """ raise NotImplementedError def __getstate__(self): """Support the pickle protocol.""" return dict(serialized=self.SerializePartialToString()) def __setstate__(self, state): """Support the pickle protocol.""" self.__init__() serialized = state['serialized'] # On Python 3, using encoding='latin1' is required for unpickling # protos pickled by Python 2. if not isinstance(serialized, bytes): serialized = serialized.encode('latin1') self.ParseFromString(serialized) def __reduce__(self): message_descriptor = self.DESCRIPTOR if message_descriptor.containing_type is None: return type(self), (), self.__getstate__() # the message type must be nested. # Python does not pickle nested classes; use the symbol_database on the # receiving end. container = message_descriptor return (_InternalConstructMessage, (container.full_name,), self.__getstate__()) def _InternalConstructMessage(full_name): """Constructs a nested message.""" from google.protobuf import symbol_database # pylint:disable=g-import-not-at-top return symbol_database.Default().GetSymbol(full_name)() ================================================ FILE: openpype/hosts/hiero/vendor/google/protobuf/message_factory.py ================================================ # Protocol Buffers - Google's data interchange format # Copyright 2008 Google Inc. All rights reserved. # https://developers.google.com/protocol-buffers/ # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are # met: # # * Redistributions of source code must retain the above copyright # notice, this list of conditions and the following disclaimer. # * Redistributions in binary form must reproduce the above # copyright notice, this list of conditions and the following disclaimer # in the documentation and/or other materials provided with the # distribution. # * Neither the name of Google Inc. nor the names of its # contributors may be used to endorse or promote products derived from # this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. """Provides a factory class for generating dynamic messages. The easiest way to use this class is if you have access to the FileDescriptor protos containing the messages you want to create you can just do the following: message_classes = message_factory.GetMessages(iterable_of_file_descriptors) my_proto_instance = message_classes['some.proto.package.MessageName']() """ __author__ = 'matthewtoia@google.com (Matt Toia)' from google.protobuf.internal import api_implementation from google.protobuf import descriptor_pool from google.protobuf import message if api_implementation.Type() == 'cpp': from google.protobuf.pyext import cpp_message as message_impl else: from google.protobuf.internal import python_message as message_impl # The type of all Message classes. _GENERATED_PROTOCOL_MESSAGE_TYPE = message_impl.GeneratedProtocolMessageType class MessageFactory(object): """Factory for creating Proto2 messages from descriptors in a pool.""" def __init__(self, pool=None): """Initializes a new factory.""" self.pool = pool or descriptor_pool.DescriptorPool() # local cache of all classes built from protobuf descriptors self._classes = {} def GetPrototype(self, descriptor): """Obtains a proto2 message class based on the passed in descriptor. Passing a descriptor with a fully qualified name matching a previous invocation will cause the same class to be returned. Args: descriptor: The descriptor to build from. Returns: A class describing the passed in descriptor. """ if descriptor not in self._classes: result_class = self.CreatePrototype(descriptor) # The assignment to _classes is redundant for the base implementation, but # might avoid confusion in cases where CreatePrototype gets overridden and # does not call the base implementation. self._classes[descriptor] = result_class return result_class return self._classes[descriptor] def CreatePrototype(self, descriptor): """Builds a proto2 message class based on the passed in descriptor. Don't call this function directly, it always creates a new class. Call GetPrototype() instead. This method is meant to be overridden in subblasses to perform additional operations on the newly constructed class. Args: descriptor: The descriptor to build from. Returns: A class describing the passed in descriptor. """ descriptor_name = descriptor.name result_class = _GENERATED_PROTOCOL_MESSAGE_TYPE( descriptor_name, (message.Message,), { 'DESCRIPTOR': descriptor, # If module not set, it wrongly points to message_factory module. '__module__': None, }) result_class._FACTORY = self # pylint: disable=protected-access # Assign in _classes before doing recursive calls to avoid infinite # recursion. self._classes[descriptor] = result_class for field in descriptor.fields: if field.message_type: self.GetPrototype(field.message_type) for extension in result_class.DESCRIPTOR.extensions: if extension.containing_type not in self._classes: self.GetPrototype(extension.containing_type) extended_class = self._classes[extension.containing_type] extended_class.RegisterExtension(extension) return result_class def GetMessages(self, files): """Gets all the messages from a specified file. This will find and resolve dependencies, failing if the descriptor pool cannot satisfy them. Args: files: The file names to extract messages from. Returns: A dictionary mapping proto names to the message classes. This will include any dependent messages as well as any messages defined in the same file as a specified message. """ result = {} for file_name in files: file_desc = self.pool.FindFileByName(file_name) for desc in file_desc.message_types_by_name.values(): result[desc.full_name] = self.GetPrototype(desc) # While the extension FieldDescriptors are created by the descriptor pool, # the python classes created in the factory need them to be registered # explicitly, which is done below. # # The call to RegisterExtension will specifically check if the # extension was already registered on the object and either # ignore the registration if the original was the same, or raise # an error if they were different. for extension in file_desc.extensions_by_name.values(): if extension.containing_type not in self._classes: self.GetPrototype(extension.containing_type) extended_class = self._classes[extension.containing_type] extended_class.RegisterExtension(extension) return result _FACTORY = MessageFactory() def GetMessages(file_protos): """Builds a dictionary of all the messages available in a set of files. Args: file_protos: Iterable of FileDescriptorProto to build messages out of. Returns: A dictionary mapping proto names to the message classes. This will include any dependent messages as well as any messages defined in the same file as a specified message. """ # The cpp implementation of the protocol buffer library requires to add the # message in topological order of the dependency graph. file_by_name = {file_proto.name: file_proto for file_proto in file_protos} def _AddFile(file_proto): for dependency in file_proto.dependency: if dependency in file_by_name: # Remove from elements to be visited, in order to cut cycles. _AddFile(file_by_name.pop(dependency)) _FACTORY.pool.Add(file_proto) while file_by_name: _AddFile(file_by_name.popitem()[1]) return _FACTORY.GetMessages([file_proto.name for file_proto in file_protos]) ================================================ FILE: openpype/hosts/hiero/vendor/google/protobuf/proto_builder.py ================================================ # Protocol Buffers - Google's data interchange format # Copyright 2008 Google Inc. All rights reserved. # https://developers.google.com/protocol-buffers/ # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are # met: # # * Redistributions of source code must retain the above copyright # notice, this list of conditions and the following disclaimer. # * Redistributions in binary form must reproduce the above # copyright notice, this list of conditions and the following disclaimer # in the documentation and/or other materials provided with the # distribution. # * Neither the name of Google Inc. nor the names of its # contributors may be used to endorse or promote products derived from # this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. """Dynamic Protobuf class creator.""" from collections import OrderedDict import hashlib import os from google.protobuf import descriptor_pb2 from google.protobuf import descriptor from google.protobuf import message_factory def _GetMessageFromFactory(factory, full_name): """Get a proto class from the MessageFactory by name. Args: factory: a MessageFactory instance. full_name: str, the fully qualified name of the proto type. Returns: A class, for the type identified by full_name. Raises: KeyError, if the proto is not found in the factory's descriptor pool. """ proto_descriptor = factory.pool.FindMessageTypeByName(full_name) proto_cls = factory.GetPrototype(proto_descriptor) return proto_cls def MakeSimpleProtoClass(fields, full_name=None, pool=None): """Create a Protobuf class whose fields are basic types. Note: this doesn't validate field names! Args: fields: dict of {name: field_type} mappings for each field in the proto. If this is an OrderedDict the order will be maintained, otherwise the fields will be sorted by name. full_name: optional str, the fully-qualified name of the proto type. pool: optional DescriptorPool instance. Returns: a class, the new protobuf class with a FileDescriptor. """ factory = message_factory.MessageFactory(pool=pool) if full_name is not None: try: proto_cls = _GetMessageFromFactory(factory, full_name) return proto_cls except KeyError: # The factory's DescriptorPool doesn't know about this class yet. pass # Get a list of (name, field_type) tuples from the fields dict. If fields was # an OrderedDict we keep the order, but otherwise we sort the field to ensure # consistent ordering. field_items = fields.items() if not isinstance(fields, OrderedDict): field_items = sorted(field_items) # Use a consistent file name that is unlikely to conflict with any imported # proto files. fields_hash = hashlib.sha1() for f_name, f_type in field_items: fields_hash.update(f_name.encode('utf-8')) fields_hash.update(str(f_type).encode('utf-8')) proto_file_name = fields_hash.hexdigest() + '.proto' # If the proto is anonymous, use the same hash to name it. if full_name is None: full_name = ('net.proto2.python.public.proto_builder.AnonymousProto_' + fields_hash.hexdigest()) try: proto_cls = _GetMessageFromFactory(factory, full_name) return proto_cls except KeyError: # The factory's DescriptorPool doesn't know about this class yet. pass # This is the first time we see this proto: add a new descriptor to the pool. factory.pool.Add( _MakeFileDescriptorProto(proto_file_name, full_name, field_items)) return _GetMessageFromFactory(factory, full_name) def _MakeFileDescriptorProto(proto_file_name, full_name, field_items): """Populate FileDescriptorProto for MessageFactory's DescriptorPool.""" package, name = full_name.rsplit('.', 1) file_proto = descriptor_pb2.FileDescriptorProto() file_proto.name = os.path.join(package.replace('.', '/'), proto_file_name) file_proto.package = package desc_proto = file_proto.message_type.add() desc_proto.name = name for f_number, (f_name, f_type) in enumerate(field_items, 1): field_proto = desc_proto.field.add() field_proto.name = f_name # # If the number falls in the reserved range, reassign it to the correct # # number after the range. if f_number >= descriptor.FieldDescriptor.FIRST_RESERVED_FIELD_NUMBER: f_number += ( descriptor.FieldDescriptor.LAST_RESERVED_FIELD_NUMBER - descriptor.FieldDescriptor.FIRST_RESERVED_FIELD_NUMBER + 1) field_proto.number = f_number field_proto.label = descriptor_pb2.FieldDescriptorProto.LABEL_OPTIONAL field_proto.type = f_type return file_proto ================================================ FILE: openpype/hosts/hiero/vendor/google/protobuf/pyext/__init__.py ================================================ ================================================ FILE: openpype/hosts/hiero/vendor/google/protobuf/pyext/cpp_message.py ================================================ # Protocol Buffers - Google's data interchange format # Copyright 2008 Google Inc. All rights reserved. # https://developers.google.com/protocol-buffers/ # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are # met: # # * Redistributions of source code must retain the above copyright # notice, this list of conditions and the following disclaimer. # * Redistributions in binary form must reproduce the above # copyright notice, this list of conditions and the following disclaimer # in the documentation and/or other materials provided with the # distribution. # * Neither the name of Google Inc. nor the names of its # contributors may be used to endorse or promote products derived from # this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. """Protocol message implementation hooks for C++ implementation. Contains helper functions used to create protocol message classes from Descriptor objects at runtime backed by the protocol buffer C++ API. """ __author__ = 'tibell@google.com (Johan Tibell)' from google.protobuf.pyext import _message class GeneratedProtocolMessageType(_message.MessageMeta): """Metaclass for protocol message classes created at runtime from Descriptors. The protocol compiler currently uses this metaclass to create protocol message classes at runtime. Clients can also manually create their own classes at runtime, as in this example: mydescriptor = Descriptor(.....) factory = symbol_database.Default() factory.pool.AddDescriptor(mydescriptor) MyProtoClass = factory.GetPrototype(mydescriptor) myproto_instance = MyProtoClass() myproto.foo_field = 23 ... The above example will not work for nested types. If you wish to include them, use reflection.MakeClass() instead of manually instantiating the class in order to create the appropriate class structure. """ # Must be consistent with the protocol-compiler code in # proto2/compiler/internal/generator.*. _DESCRIPTOR_KEY = 'DESCRIPTOR' ================================================ FILE: openpype/hosts/hiero/vendor/google/protobuf/pyext/python_pb2.py ================================================ # -*- coding: utf-8 -*- # Generated by the protocol buffer compiler. DO NOT EDIT! # source: google/protobuf/pyext/python.proto """Generated protocol buffer code.""" from google.protobuf.internal import builder as _builder from google.protobuf import descriptor as _descriptor from google.protobuf import descriptor_pool as _descriptor_pool from google.protobuf import symbol_database as _symbol_database # @@protoc_insertion_point(imports) _sym_db = _symbol_database.Default() DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\"google/protobuf/pyext/python.proto\x12\x1fgoogle.protobuf.python.internal\"\xbc\x02\n\x0cTestAllTypes\x12\\\n\x17repeated_nested_message\x18\x01 \x03(\x0b\x32;.google.protobuf.python.internal.TestAllTypes.NestedMessage\x12\\\n\x17optional_nested_message\x18\x02 \x01(\x0b\x32;.google.protobuf.python.internal.TestAllTypes.NestedMessage\x12\x16\n\x0eoptional_int32\x18\x03 \x01(\x05\x1aX\n\rNestedMessage\x12\n\n\x02\x62\x62\x18\x01 \x01(\x05\x12;\n\x02\x63\x63\x18\x02 \x01(\x0b\x32/.google.protobuf.python.internal.ForeignMessage\"&\n\x0e\x46oreignMessage\x12\t\n\x01\x63\x18\x01 \x01(\x05\x12\t\n\x01\x64\x18\x02 \x03(\x05\"\x1d\n\x11TestAllExtensions*\x08\x08\x01\x10\x80\x80\x80\x80\x02:\x9a\x01\n!optional_nested_message_extension\x12\x32.google.protobuf.python.internal.TestAllExtensions\x18\x01 \x01(\x0b\x32;.google.protobuf.python.internal.TestAllTypes.NestedMessage:\x9a\x01\n!repeated_nested_message_extension\x12\x32.google.protobuf.python.internal.TestAllExtensions\x18\x02 \x03(\x0b\x32;.google.protobuf.python.internal.TestAllTypes.NestedMessageB\x02H\x01') _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals()) _builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'google.protobuf.pyext.python_pb2', globals()) if _descriptor._USE_C_DESCRIPTORS == False: TestAllExtensions.RegisterExtension(optional_nested_message_extension) TestAllExtensions.RegisterExtension(repeated_nested_message_extension) DESCRIPTOR._options = None DESCRIPTOR._serialized_options = b'H\001' _TESTALLTYPES._serialized_start=72 _TESTALLTYPES._serialized_end=388 _TESTALLTYPES_NESTEDMESSAGE._serialized_start=300 _TESTALLTYPES_NESTEDMESSAGE._serialized_end=388 _FOREIGNMESSAGE._serialized_start=390 _FOREIGNMESSAGE._serialized_end=428 _TESTALLEXTENSIONS._serialized_start=430 _TESTALLEXTENSIONS._serialized_end=459 # @@protoc_insertion_point(module_scope) ================================================ FILE: openpype/hosts/hiero/vendor/google/protobuf/reflection.py ================================================ # Protocol Buffers - Google's data interchange format # Copyright 2008 Google Inc. All rights reserved. # https://developers.google.com/protocol-buffers/ # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are # met: # # * Redistributions of source code must retain the above copyright # notice, this list of conditions and the following disclaimer. # * Redistributions in binary form must reproduce the above # copyright notice, this list of conditions and the following disclaimer # in the documentation and/or other materials provided with the # distribution. # * Neither the name of Google Inc. nor the names of its # contributors may be used to endorse or promote products derived from # this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. # This code is meant to work on Python 2.4 and above only. """Contains a metaclass and helper functions used to create protocol message classes from Descriptor objects at runtime. Recall that a metaclass is the "type" of a class. (A class is to a metaclass what an instance is to a class.) In this case, we use the GeneratedProtocolMessageType metaclass to inject all the useful functionality into the classes output by the protocol compiler at compile-time. The upshot of all this is that the real implementation details for ALL pure-Python protocol buffers are *here in this file*. """ __author__ = 'robinson@google.com (Will Robinson)' from google.protobuf import message_factory from google.protobuf import symbol_database # The type of all Message classes. # Part of the public interface, but normally only used by message factories. GeneratedProtocolMessageType = message_factory._GENERATED_PROTOCOL_MESSAGE_TYPE MESSAGE_CLASS_CACHE = {} # Deprecated. Please NEVER use reflection.ParseMessage(). def ParseMessage(descriptor, byte_str): """Generate a new Message instance from this Descriptor and a byte string. DEPRECATED: ParseMessage is deprecated because it is using MakeClass(). Please use MessageFactory.GetPrototype() instead. Args: descriptor: Protobuf Descriptor object byte_str: Serialized protocol buffer byte string Returns: Newly created protobuf Message object. """ result_class = MakeClass(descriptor) new_msg = result_class() new_msg.ParseFromString(byte_str) return new_msg # Deprecated. Please NEVER use reflection.MakeClass(). def MakeClass(descriptor): """Construct a class object for a protobuf described by descriptor. DEPRECATED: use MessageFactory.GetPrototype() instead. Args: descriptor: A descriptor.Descriptor object describing the protobuf. Returns: The Message class object described by the descriptor. """ # Original implementation leads to duplicate message classes, which won't play # well with extensions. Message factory info is also missing. # Redirect to message_factory. return symbol_database.Default().GetPrototype(descriptor) ================================================ FILE: openpype/hosts/hiero/vendor/google/protobuf/service.py ================================================ # Protocol Buffers - Google's data interchange format # Copyright 2008 Google Inc. All rights reserved. # https://developers.google.com/protocol-buffers/ # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are # met: # # * Redistributions of source code must retain the above copyright # notice, this list of conditions and the following disclaimer. # * Redistributions in binary form must reproduce the above # copyright notice, this list of conditions and the following disclaimer # in the documentation and/or other materials provided with the # distribution. # * Neither the name of Google Inc. nor the names of its # contributors may be used to endorse or promote products derived from # this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. """DEPRECATED: Declares the RPC service interfaces. This module declares the abstract interfaces underlying proto2 RPC services. These are intended to be independent of any particular RPC implementation, so that proto2 services can be used on top of a variety of implementations. Starting with version 2.3.0, RPC implementations should not try to build on these, but should instead provide code generator plugins which generate code specific to the particular RPC implementation. This way the generated code can be more appropriate for the implementation in use and can avoid unnecessary layers of indirection. """ __author__ = 'petar@google.com (Petar Petrov)' class RpcException(Exception): """Exception raised on failed blocking RPC method call.""" pass class Service(object): """Abstract base interface for protocol-buffer-based RPC services. Services themselves are abstract classes (implemented either by servers or as stubs), but they subclass this base interface. The methods of this interface can be used to call the methods of the service without knowing its exact type at compile time (analogous to the Message interface). """ def GetDescriptor(): """Retrieves this service's descriptor.""" raise NotImplementedError def CallMethod(self, method_descriptor, rpc_controller, request, done): """Calls a method of the service specified by method_descriptor. If "done" is None then the call is blocking and the response message will be returned directly. Otherwise the call is asynchronous and "done" will later be called with the response value. In the blocking case, RpcException will be raised on error. Preconditions: * method_descriptor.service == GetDescriptor * request is of the exact same classes as returned by GetRequestClass(method). * After the call has started, the request must not be modified. * "rpc_controller" is of the correct type for the RPC implementation being used by this Service. For stubs, the "correct type" depends on the RpcChannel which the stub is using. Postconditions: * "done" will be called when the method is complete. This may be before CallMethod() returns or it may be at some point in the future. * If the RPC failed, the response value passed to "done" will be None. Further details about the failure can be found by querying the RpcController. """ raise NotImplementedError def GetRequestClass(self, method_descriptor): """Returns the class of the request message for the specified method. CallMethod() requires that the request is of a particular subclass of Message. GetRequestClass() gets the default instance of this required type. Example: method = service.GetDescriptor().FindMethodByName("Foo") request = stub.GetRequestClass(method)() request.ParseFromString(input) service.CallMethod(method, request, callback) """ raise NotImplementedError def GetResponseClass(self, method_descriptor): """Returns the class of the response message for the specified method. This method isn't really needed, as the RpcChannel's CallMethod constructs the response protocol message. It's provided anyway in case it is useful for the caller to know the response type in advance. """ raise NotImplementedError class RpcController(object): """An RpcController mediates a single method call. The primary purpose of the controller is to provide a way to manipulate settings specific to the RPC implementation and to find out about RPC-level errors. The methods provided by the RpcController interface are intended to be a "least common denominator" set of features which we expect all implementations to support. Specific implementations may provide more advanced features (e.g. deadline propagation). """ # Client-side methods below def Reset(self): """Resets the RpcController to its initial state. After the RpcController has been reset, it may be reused in a new call. Must not be called while an RPC is in progress. """ raise NotImplementedError def Failed(self): """Returns true if the call failed. After a call has finished, returns true if the call failed. The possible reasons for failure depend on the RPC implementation. Failed() must not be called before a call has finished. If Failed() returns true, the contents of the response message are undefined. """ raise NotImplementedError def ErrorText(self): """If Failed is true, returns a human-readable description of the error.""" raise NotImplementedError def StartCancel(self): """Initiate cancellation. Advises the RPC system that the caller desires that the RPC call be canceled. The RPC system may cancel it immediately, may wait awhile and then cancel it, or may not even cancel the call at all. If the call is canceled, the "done" callback will still be called and the RpcController will indicate that the call failed at that time. """ raise NotImplementedError # Server-side methods below def SetFailed(self, reason): """Sets a failure reason. Causes Failed() to return true on the client side. "reason" will be incorporated into the message returned by ErrorText(). If you find you need to return machine-readable information about failures, you should incorporate it into your response protocol buffer and should NOT call SetFailed(). """ raise NotImplementedError def IsCanceled(self): """Checks if the client cancelled the RPC. If true, indicates that the client canceled the RPC, so the server may as well give up on replying to it. The server should still call the final "done" callback. """ raise NotImplementedError def NotifyOnCancel(self, callback): """Sets a callback to invoke on cancel. Asks that the given callback be called when the RPC is canceled. The callback will always be called exactly once. If the RPC completes without being canceled, the callback will be called after completion. If the RPC has already been canceled when NotifyOnCancel() is called, the callback will be called immediately. NotifyOnCancel() must be called no more than once per request. """ raise NotImplementedError class RpcChannel(object): """Abstract interface for an RPC channel. An RpcChannel represents a communication line to a service which can be used to call that service's methods. The service may be running on another machine. Normally, you should not use an RpcChannel directly, but instead construct a stub {@link Service} wrapping it. Example: Example: RpcChannel channel = rpcImpl.Channel("remotehost.example.com:1234") RpcController controller = rpcImpl.Controller() MyService service = MyService_Stub(channel) service.MyMethod(controller, request, callback) """ def CallMethod(self, method_descriptor, rpc_controller, request, response_class, done): """Calls the method identified by the descriptor. Call the given method of the remote service. The signature of this procedure looks the same as Service.CallMethod(), but the requirements are less strict in one important way: the request object doesn't have to be of any specific class as long as its descriptor is method.input_type. """ raise NotImplementedError ================================================ FILE: openpype/hosts/hiero/vendor/google/protobuf/service_reflection.py ================================================ # Protocol Buffers - Google's data interchange format # Copyright 2008 Google Inc. All rights reserved. # https://developers.google.com/protocol-buffers/ # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are # met: # # * Redistributions of source code must retain the above copyright # notice, this list of conditions and the following disclaimer. # * Redistributions in binary form must reproduce the above # copyright notice, this list of conditions and the following disclaimer # in the documentation and/or other materials provided with the # distribution. # * Neither the name of Google Inc. nor the names of its # contributors may be used to endorse or promote products derived from # this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. """Contains metaclasses used to create protocol service and service stub classes from ServiceDescriptor objects at runtime. The GeneratedServiceType and GeneratedServiceStubType metaclasses are used to inject all useful functionality into the classes output by the protocol compiler at compile-time. """ __author__ = 'petar@google.com (Petar Petrov)' class GeneratedServiceType(type): """Metaclass for service classes created at runtime from ServiceDescriptors. Implementations for all methods described in the Service class are added here by this class. We also create properties to allow getting/setting all fields in the protocol message. The protocol compiler currently uses this metaclass to create protocol service classes at runtime. Clients can also manually create their own classes at runtime, as in this example:: mydescriptor = ServiceDescriptor(.....) class MyProtoService(service.Service): __metaclass__ = GeneratedServiceType DESCRIPTOR = mydescriptor myservice_instance = MyProtoService() # ... """ _DESCRIPTOR_KEY = 'DESCRIPTOR' def __init__(cls, name, bases, dictionary): """Creates a message service class. Args: name: Name of the class (ignored, but required by the metaclass protocol). bases: Base classes of the class being constructed. dictionary: The class dictionary of the class being constructed. dictionary[_DESCRIPTOR_KEY] must contain a ServiceDescriptor object describing this protocol service type. """ # Don't do anything if this class doesn't have a descriptor. This happens # when a service class is subclassed. if GeneratedServiceType._DESCRIPTOR_KEY not in dictionary: return descriptor = dictionary[GeneratedServiceType._DESCRIPTOR_KEY] service_builder = _ServiceBuilder(descriptor) service_builder.BuildService(cls) cls.DESCRIPTOR = descriptor class GeneratedServiceStubType(GeneratedServiceType): """Metaclass for service stubs created at runtime from ServiceDescriptors. This class has similar responsibilities as GeneratedServiceType, except that it creates the service stub classes. """ _DESCRIPTOR_KEY = 'DESCRIPTOR' def __init__(cls, name, bases, dictionary): """Creates a message service stub class. Args: name: Name of the class (ignored, here). bases: Base classes of the class being constructed. dictionary: The class dictionary of the class being constructed. dictionary[_DESCRIPTOR_KEY] must contain a ServiceDescriptor object describing this protocol service type. """ super(GeneratedServiceStubType, cls).__init__(name, bases, dictionary) # Don't do anything if this class doesn't have a descriptor. This happens # when a service stub is subclassed. if GeneratedServiceStubType._DESCRIPTOR_KEY not in dictionary: return descriptor = dictionary[GeneratedServiceStubType._DESCRIPTOR_KEY] service_stub_builder = _ServiceStubBuilder(descriptor) service_stub_builder.BuildServiceStub(cls) class _ServiceBuilder(object): """This class constructs a protocol service class using a service descriptor. Given a service descriptor, this class constructs a class that represents the specified service descriptor. One service builder instance constructs exactly one service class. That means all instances of that class share the same builder. """ def __init__(self, service_descriptor): """Initializes an instance of the service class builder. Args: service_descriptor: ServiceDescriptor to use when constructing the service class. """ self.descriptor = service_descriptor def BuildService(builder, cls): """Constructs the service class. Args: cls: The class that will be constructed. """ # CallMethod needs to operate with an instance of the Service class. This # internal wrapper function exists only to be able to pass the service # instance to the method that does the real CallMethod work. # Making sure to use exact argument names from the abstract interface in # service.py to match the type signature def _WrapCallMethod(self, method_descriptor, rpc_controller, request, done): return builder._CallMethod(self, method_descriptor, rpc_controller, request, done) def _WrapGetRequestClass(self, method_descriptor): return builder._GetRequestClass(method_descriptor) def _WrapGetResponseClass(self, method_descriptor): return builder._GetResponseClass(method_descriptor) builder.cls = cls cls.CallMethod = _WrapCallMethod cls.GetDescriptor = staticmethod(lambda: builder.descriptor) cls.GetDescriptor.__doc__ = 'Returns the service descriptor.' cls.GetRequestClass = _WrapGetRequestClass cls.GetResponseClass = _WrapGetResponseClass for method in builder.descriptor.methods: setattr(cls, method.name, builder._GenerateNonImplementedMethod(method)) def _CallMethod(self, srvc, method_descriptor, rpc_controller, request, callback): """Calls the method described by a given method descriptor. Args: srvc: Instance of the service for which this method is called. method_descriptor: Descriptor that represent the method to call. rpc_controller: RPC controller to use for this method's execution. request: Request protocol message. callback: A callback to invoke after the method has completed. """ if method_descriptor.containing_service != self.descriptor: raise RuntimeError( 'CallMethod() given method descriptor for wrong service type.') method = getattr(srvc, method_descriptor.name) return method(rpc_controller, request, callback) def _GetRequestClass(self, method_descriptor): """Returns the class of the request protocol message. Args: method_descriptor: Descriptor of the method for which to return the request protocol message class. Returns: A class that represents the input protocol message of the specified method. """ if method_descriptor.containing_service != self.descriptor: raise RuntimeError( 'GetRequestClass() given method descriptor for wrong service type.') return method_descriptor.input_type._concrete_class def _GetResponseClass(self, method_descriptor): """Returns the class of the response protocol message. Args: method_descriptor: Descriptor of the method for which to return the response protocol message class. Returns: A class that represents the output protocol message of the specified method. """ if method_descriptor.containing_service != self.descriptor: raise RuntimeError( 'GetResponseClass() given method descriptor for wrong service type.') return method_descriptor.output_type._concrete_class def _GenerateNonImplementedMethod(self, method): """Generates and returns a method that can be set for a service methods. Args: method: Descriptor of the service method for which a method is to be generated. Returns: A method that can be added to the service class. """ return lambda inst, rpc_controller, request, callback: ( self._NonImplementedMethod(method.name, rpc_controller, callback)) def _NonImplementedMethod(self, method_name, rpc_controller, callback): """The body of all methods in the generated service class. Args: method_name: Name of the method being executed. rpc_controller: RPC controller used to execute this method. callback: A callback which will be invoked when the method finishes. """ rpc_controller.SetFailed('Method %s not implemented.' % method_name) callback(None) class _ServiceStubBuilder(object): """Constructs a protocol service stub class using a service descriptor. Given a service descriptor, this class constructs a suitable stub class. A stub is just a type-safe wrapper around an RpcChannel which emulates a local implementation of the service. One service stub builder instance constructs exactly one class. It means all instances of that class share the same service stub builder. """ def __init__(self, service_descriptor): """Initializes an instance of the service stub class builder. Args: service_descriptor: ServiceDescriptor to use when constructing the stub class. """ self.descriptor = service_descriptor def BuildServiceStub(self, cls): """Constructs the stub class. Args: cls: The class that will be constructed. """ def _ServiceStubInit(stub, rpc_channel): stub.rpc_channel = rpc_channel self.cls = cls cls.__init__ = _ServiceStubInit for method in self.descriptor.methods: setattr(cls, method.name, self._GenerateStubMethod(method)) def _GenerateStubMethod(self, method): return (lambda inst, rpc_controller, request, callback=None: self._StubMethod(inst, method, rpc_controller, request, callback)) def _StubMethod(self, stub, method_descriptor, rpc_controller, request, callback): """The body of all service methods in the generated stub class. Args: stub: Stub instance. method_descriptor: Descriptor of the invoked method. rpc_controller: Rpc controller to execute the method. request: Request protocol message. callback: A callback to execute when the method finishes. Returns: Response message (in case of blocking call). """ return stub.rpc_channel.CallMethod( method_descriptor, rpc_controller, request, method_descriptor.output_type._concrete_class, callback) ================================================ FILE: openpype/hosts/hiero/vendor/google/protobuf/source_context_pb2.py ================================================ # -*- coding: utf-8 -*- # Generated by the protocol buffer compiler. DO NOT EDIT! # source: google/protobuf/source_context.proto """Generated protocol buffer code.""" from google.protobuf.internal import builder as _builder from google.protobuf import descriptor as _descriptor from google.protobuf import descriptor_pool as _descriptor_pool from google.protobuf import symbol_database as _symbol_database # @@protoc_insertion_point(imports) _sym_db = _symbol_database.Default() DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n$google/protobuf/source_context.proto\x12\x0fgoogle.protobuf\"\"\n\rSourceContext\x12\x11\n\tfile_name\x18\x01 \x01(\tB\x8a\x01\n\x13\x63om.google.protobufB\x12SourceContextProtoP\x01Z6google.golang.org/protobuf/types/known/sourcecontextpb\xa2\x02\x03GPB\xaa\x02\x1eGoogle.Protobuf.WellKnownTypesb\x06proto3') _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals()) _builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'google.protobuf.source_context_pb2', globals()) if _descriptor._USE_C_DESCRIPTORS == False: DESCRIPTOR._options = None DESCRIPTOR._serialized_options = b'\n\023com.google.protobufB\022SourceContextProtoP\001Z6google.golang.org/protobuf/types/known/sourcecontextpb\242\002\003GPB\252\002\036Google.Protobuf.WellKnownTypes' _SOURCECONTEXT._serialized_start=57 _SOURCECONTEXT._serialized_end=91 # @@protoc_insertion_point(module_scope) ================================================ FILE: openpype/hosts/hiero/vendor/google/protobuf/struct_pb2.py ================================================ # -*- coding: utf-8 -*- # Generated by the protocol buffer compiler. DO NOT EDIT! # source: google/protobuf/struct.proto """Generated protocol buffer code.""" from google.protobuf.internal import builder as _builder from google.protobuf import descriptor as _descriptor from google.protobuf import descriptor_pool as _descriptor_pool from google.protobuf import symbol_database as _symbol_database # @@protoc_insertion_point(imports) _sym_db = _symbol_database.Default() DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x1cgoogle/protobuf/struct.proto\x12\x0fgoogle.protobuf\"\x84\x01\n\x06Struct\x12\x33\n\x06\x66ields\x18\x01 \x03(\x0b\x32#.google.protobuf.Struct.FieldsEntry\x1a\x45\n\x0b\x46ieldsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12%\n\x05value\x18\x02 \x01(\x0b\x32\x16.google.protobuf.Value:\x02\x38\x01\"\xea\x01\n\x05Value\x12\x30\n\nnull_value\x18\x01 \x01(\x0e\x32\x1a.google.protobuf.NullValueH\x00\x12\x16\n\x0cnumber_value\x18\x02 \x01(\x01H\x00\x12\x16\n\x0cstring_value\x18\x03 \x01(\tH\x00\x12\x14\n\nbool_value\x18\x04 \x01(\x08H\x00\x12/\n\x0cstruct_value\x18\x05 \x01(\x0b\x32\x17.google.protobuf.StructH\x00\x12\x30\n\nlist_value\x18\x06 \x01(\x0b\x32\x1a.google.protobuf.ListValueH\x00\x42\x06\n\x04kind\"3\n\tListValue\x12&\n\x06values\x18\x01 \x03(\x0b\x32\x16.google.protobuf.Value*\x1b\n\tNullValue\x12\x0e\n\nNULL_VALUE\x10\x00\x42\x7f\n\x13\x63om.google.protobufB\x0bStructProtoP\x01Z/google.golang.org/protobuf/types/known/structpb\xf8\x01\x01\xa2\x02\x03GPB\xaa\x02\x1eGoogle.Protobuf.WellKnownTypesb\x06proto3') _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals()) _builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'google.protobuf.struct_pb2', globals()) if _descriptor._USE_C_DESCRIPTORS == False: DESCRIPTOR._options = None DESCRIPTOR._serialized_options = b'\n\023com.google.protobufB\013StructProtoP\001Z/google.golang.org/protobuf/types/known/structpb\370\001\001\242\002\003GPB\252\002\036Google.Protobuf.WellKnownTypes' _STRUCT_FIELDSENTRY._options = None _STRUCT_FIELDSENTRY._serialized_options = b'8\001' _NULLVALUE._serialized_start=474 _NULLVALUE._serialized_end=501 _STRUCT._serialized_start=50 _STRUCT._serialized_end=182 _STRUCT_FIELDSENTRY._serialized_start=113 _STRUCT_FIELDSENTRY._serialized_end=182 _VALUE._serialized_start=185 _VALUE._serialized_end=419 _LISTVALUE._serialized_start=421 _LISTVALUE._serialized_end=472 # @@protoc_insertion_point(module_scope) ================================================ FILE: openpype/hosts/hiero/vendor/google/protobuf/symbol_database.py ================================================ # Protocol Buffers - Google's data interchange format # Copyright 2008 Google Inc. All rights reserved. # https://developers.google.com/protocol-buffers/ # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are # met: # # * Redistributions of source code must retain the above copyright # notice, this list of conditions and the following disclaimer. # * Redistributions in binary form must reproduce the above # copyright notice, this list of conditions and the following disclaimer # in the documentation and/or other materials provided with the # distribution. # * Neither the name of Google Inc. nor the names of its # contributors may be used to endorse or promote products derived from # this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. """A database of Python protocol buffer generated symbols. SymbolDatabase is the MessageFactory for messages generated at compile time, and makes it easy to create new instances of a registered type, given only the type's protocol buffer symbol name. Example usage:: db = symbol_database.SymbolDatabase() # Register symbols of interest, from one or multiple files. db.RegisterFileDescriptor(my_proto_pb2.DESCRIPTOR) db.RegisterMessage(my_proto_pb2.MyMessage) db.RegisterEnumDescriptor(my_proto_pb2.MyEnum.DESCRIPTOR) # The database can be used as a MessageFactory, to generate types based on # their name: types = db.GetMessages(['my_proto.proto']) my_message_instance = types['MyMessage']() # The database's underlying descriptor pool can be queried, so it's not # necessary to know a type's filename to be able to generate it: filename = db.pool.FindFileContainingSymbol('MyMessage') my_message_instance = db.GetMessages([filename])['MyMessage']() # This functionality is also provided directly via a convenience method: my_message_instance = db.GetSymbol('MyMessage')() """ from google.protobuf.internal import api_implementation from google.protobuf import descriptor_pool from google.protobuf import message_factory class SymbolDatabase(message_factory.MessageFactory): """A database of Python generated symbols.""" def RegisterMessage(self, message): """Registers the given message type in the local database. Calls to GetSymbol() and GetMessages() will return messages registered here. Args: message: A :class:`google.protobuf.message.Message` subclass (or instance); its descriptor will be registered. Returns: The provided message. """ desc = message.DESCRIPTOR self._classes[desc] = message self.RegisterMessageDescriptor(desc) return message def RegisterMessageDescriptor(self, message_descriptor): """Registers the given message descriptor in the local database. Args: message_descriptor (Descriptor): the message descriptor to add. """ if api_implementation.Type() == 'python': # pylint: disable=protected-access self.pool._AddDescriptor(message_descriptor) def RegisterEnumDescriptor(self, enum_descriptor): """Registers the given enum descriptor in the local database. Args: enum_descriptor (EnumDescriptor): The enum descriptor to register. Returns: EnumDescriptor: The provided descriptor. """ if api_implementation.Type() == 'python': # pylint: disable=protected-access self.pool._AddEnumDescriptor(enum_descriptor) return enum_descriptor def RegisterServiceDescriptor(self, service_descriptor): """Registers the given service descriptor in the local database. Args: service_descriptor (ServiceDescriptor): the service descriptor to register. """ if api_implementation.Type() == 'python': # pylint: disable=protected-access self.pool._AddServiceDescriptor(service_descriptor) def RegisterFileDescriptor(self, file_descriptor): """Registers the given file descriptor in the local database. Args: file_descriptor (FileDescriptor): The file descriptor to register. """ if api_implementation.Type() == 'python': # pylint: disable=protected-access self.pool._InternalAddFileDescriptor(file_descriptor) def GetSymbol(self, symbol): """Tries to find a symbol in the local database. Currently, this method only returns message.Message instances, however, if may be extended in future to support other symbol types. Args: symbol (str): a protocol buffer symbol. Returns: A Python class corresponding to the symbol. Raises: KeyError: if the symbol could not be found. """ return self._classes[self.pool.FindMessageTypeByName(symbol)] def GetMessages(self, files): # TODO(amauryfa): Fix the differences with MessageFactory. """Gets all registered messages from a specified file. Only messages already created and registered will be returned; (this is the case for imported _pb2 modules) But unlike MessageFactory, this version also returns already defined nested messages, but does not register any message extensions. Args: files (list[str]): The file names to extract messages from. Returns: A dictionary mapping proto names to the message classes. Raises: KeyError: if a file could not be found. """ def _GetAllMessages(desc): """Walk a message Descriptor and recursively yields all message names.""" yield desc for msg_desc in desc.nested_types: for nested_desc in _GetAllMessages(msg_desc): yield nested_desc result = {} for file_name in files: file_desc = self.pool.FindFileByName(file_name) for msg_desc in file_desc.message_types_by_name.values(): for desc in _GetAllMessages(msg_desc): try: result[desc.full_name] = self._classes[desc] except KeyError: # This descriptor has no registered class, skip it. pass return result _DEFAULT = SymbolDatabase(pool=descriptor_pool.Default()) def Default(): """Returns the default SymbolDatabase.""" return _DEFAULT ================================================ FILE: openpype/hosts/hiero/vendor/google/protobuf/text_encoding.py ================================================ # Protocol Buffers - Google's data interchange format # Copyright 2008 Google Inc. All rights reserved. # https://developers.google.com/protocol-buffers/ # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are # met: # # * Redistributions of source code must retain the above copyright # notice, this list of conditions and the following disclaimer. # * Redistributions in binary form must reproduce the above # copyright notice, this list of conditions and the following disclaimer # in the documentation and/or other materials provided with the # distribution. # * Neither the name of Google Inc. nor the names of its # contributors may be used to endorse or promote products derived from # this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. """Encoding related utilities.""" import re _cescape_chr_to_symbol_map = {} _cescape_chr_to_symbol_map[9] = r'\t' # optional escape _cescape_chr_to_symbol_map[10] = r'\n' # optional escape _cescape_chr_to_symbol_map[13] = r'\r' # optional escape _cescape_chr_to_symbol_map[34] = r'\"' # necessary escape _cescape_chr_to_symbol_map[39] = r"\'" # optional escape _cescape_chr_to_symbol_map[92] = r'\\' # necessary escape # Lookup table for unicode _cescape_unicode_to_str = [chr(i) for i in range(0, 256)] for byte, string in _cescape_chr_to_symbol_map.items(): _cescape_unicode_to_str[byte] = string # Lookup table for non-utf8, with necessary escapes at (o >= 127 or o < 32) _cescape_byte_to_str = ([r'\%03o' % i for i in range(0, 32)] + [chr(i) for i in range(32, 127)] + [r'\%03o' % i for i in range(127, 256)]) for byte, string in _cescape_chr_to_symbol_map.items(): _cescape_byte_to_str[byte] = string del byte, string def CEscape(text, as_utf8): # type: (...) -> str """Escape a bytes string for use in an text protocol buffer. Args: text: A byte string to be escaped. as_utf8: Specifies if result may contain non-ASCII characters. In Python 3 this allows unescaped non-ASCII Unicode characters. In Python 2 the return value will be valid UTF-8 rather than only ASCII. Returns: Escaped string (str). """ # Python's text.encode() 'string_escape' or 'unicode_escape' codecs do not # satisfy our needs; they encodes unprintable characters using two-digit hex # escapes whereas our C++ unescaping function allows hex escapes to be any # length. So, "\0011".encode('string_escape') ends up being "\\x011", which # will be decoded in C++ as a single-character string with char code 0x11. text_is_unicode = isinstance(text, str) if as_utf8 and text_is_unicode: # We're already unicode, no processing beyond control char escapes. return text.translate(_cescape_chr_to_symbol_map) ord_ = ord if text_is_unicode else lambda x: x # bytes iterate as ints. if as_utf8: return ''.join(_cescape_unicode_to_str[ord_(c)] for c in text) return ''.join(_cescape_byte_to_str[ord_(c)] for c in text) _CUNESCAPE_HEX = re.compile(r'(\\+)x([0-9a-fA-F])(?![0-9a-fA-F])') def CUnescape(text): # type: (str) -> bytes """Unescape a text string with C-style escape sequences to UTF-8 bytes. Args: text: The data to parse in a str. Returns: A byte string. """ def ReplaceHex(m): # Only replace the match if the number of leading back slashes is odd. i.e. # the slash itself is not escaped. if len(m.group(1)) & 1: return m.group(1) + 'x0' + m.group(2) return m.group(0) # This is required because the 'string_escape' encoding doesn't # allow single-digit hex escapes (like '\xf'). result = _CUNESCAPE_HEX.sub(ReplaceHex, text) return (result.encode('utf-8') # Make it bytes to allow decode. .decode('unicode_escape') # Make it bytes again to return the proper type. .encode('raw_unicode_escape')) ================================================ FILE: openpype/hosts/hiero/vendor/google/protobuf/text_format.py ================================================ # Protocol Buffers - Google's data interchange format # Copyright 2008 Google Inc. All rights reserved. # https://developers.google.com/protocol-buffers/ # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are # met: # # * Redistributions of source code must retain the above copyright # notice, this list of conditions and the following disclaimer. # * Redistributions in binary form must reproduce the above # copyright notice, this list of conditions and the following disclaimer # in the documentation and/or other materials provided with the # distribution. # * Neither the name of Google Inc. nor the names of its # contributors may be used to endorse or promote products derived from # this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. """Contains routines for printing protocol messages in text format. Simple usage example:: # Create a proto object and serialize it to a text proto string. message = my_proto_pb2.MyMessage(foo='bar') text_proto = text_format.MessageToString(message) # Parse a text proto string. message = text_format.Parse(text_proto, my_proto_pb2.MyMessage()) """ __author__ = 'kenton@google.com (Kenton Varda)' # TODO(b/129989314) Import thread contention leads to test failures. import encodings.raw_unicode_escape # pylint: disable=unused-import import encodings.unicode_escape # pylint: disable=unused-import import io import math import re from google.protobuf.internal import decoder from google.protobuf.internal import type_checkers from google.protobuf import descriptor from google.protobuf import text_encoding # pylint: disable=g-import-not-at-top __all__ = ['MessageToString', 'Parse', 'PrintMessage', 'PrintField', 'PrintFieldValue', 'Merge', 'MessageToBytes'] _INTEGER_CHECKERS = (type_checkers.Uint32ValueChecker(), type_checkers.Int32ValueChecker(), type_checkers.Uint64ValueChecker(), type_checkers.Int64ValueChecker()) _FLOAT_INFINITY = re.compile('-?inf(?:inity)?f?$', re.IGNORECASE) _FLOAT_NAN = re.compile('nanf?$', re.IGNORECASE) _QUOTES = frozenset(("'", '"')) _ANY_FULL_TYPE_NAME = 'google.protobuf.Any' class Error(Exception): """Top-level module error for text_format.""" class ParseError(Error): """Thrown in case of text parsing or tokenizing error.""" def __init__(self, message=None, line=None, column=None): if message is not None and line is not None: loc = str(line) if column is not None: loc += ':{0}'.format(column) message = '{0} : {1}'.format(loc, message) if message is not None: super(ParseError, self).__init__(message) else: super(ParseError, self).__init__() self._line = line self._column = column def GetLine(self): return self._line def GetColumn(self): return self._column class TextWriter(object): def __init__(self, as_utf8): self._writer = io.StringIO() def write(self, val): return self._writer.write(val) def close(self): return self._writer.close() def getvalue(self): return self._writer.getvalue() def MessageToString( message, as_utf8=False, as_one_line=False, use_short_repeated_primitives=False, pointy_brackets=False, use_index_order=False, float_format=None, double_format=None, use_field_number=False, descriptor_pool=None, indent=0, message_formatter=None, print_unknown_fields=False, force_colon=False): # type: (...) -> str """Convert protobuf message to text format. Double values can be formatted compactly with 15 digits of precision (which is the most that IEEE 754 "double" can guarantee) using double_format='.15g'. To ensure that converting to text and back to a proto will result in an identical value, double_format='.17g' should be used. Args: message: The protocol buffers message. as_utf8: Return unescaped Unicode for non-ASCII characters. In Python 3 actual Unicode characters may appear as is in strings. In Python 2 the return value will be valid UTF-8 rather than only ASCII. as_one_line: Don't introduce newlines between fields. use_short_repeated_primitives: Use short repeated format for primitives. pointy_brackets: If True, use angle brackets instead of curly braces for nesting. use_index_order: If True, fields of a proto message will be printed using the order defined in source code instead of the field number, extensions will be printed at the end of the message and their relative order is determined by the extension number. By default, use the field number order. float_format (str): If set, use this to specify float field formatting (per the "Format Specification Mini-Language"); otherwise, shortest float that has same value in wire will be printed. Also affect double field if double_format is not set but float_format is set. double_format (str): If set, use this to specify double field formatting (per the "Format Specification Mini-Language"); if it is not set but float_format is set, use float_format. Otherwise, use ``str()`` use_field_number: If True, print field numbers instead of names. descriptor_pool (DescriptorPool): Descriptor pool used to resolve Any types. indent (int): The initial indent level, in terms of spaces, for pretty print. message_formatter (function(message, indent, as_one_line) -> unicode|None): Custom formatter for selected sub-messages (usually based on message type). Use to pretty print parts of the protobuf for easier diffing. print_unknown_fields: If True, unknown fields will be printed. force_colon: If set, a colon will be added after the field name even if the field is a proto message. Returns: str: A string of the text formatted protocol buffer message. """ out = TextWriter(as_utf8) printer = _Printer( out, indent, as_utf8, as_one_line, use_short_repeated_primitives, pointy_brackets, use_index_order, float_format, double_format, use_field_number, descriptor_pool, message_formatter, print_unknown_fields=print_unknown_fields, force_colon=force_colon) printer.PrintMessage(message) result = out.getvalue() out.close() if as_one_line: return result.rstrip() return result def MessageToBytes(message, **kwargs): # type: (...) -> bytes """Convert protobuf message to encoded text format. See MessageToString.""" text = MessageToString(message, **kwargs) if isinstance(text, bytes): return text codec = 'utf-8' if kwargs.get('as_utf8') else 'ascii' return text.encode(codec) def _IsMapEntry(field): return (field.type == descriptor.FieldDescriptor.TYPE_MESSAGE and field.message_type.has_options and field.message_type.GetOptions().map_entry) def PrintMessage(message, out, indent=0, as_utf8=False, as_one_line=False, use_short_repeated_primitives=False, pointy_brackets=False, use_index_order=False, float_format=None, double_format=None, use_field_number=False, descriptor_pool=None, message_formatter=None, print_unknown_fields=False, force_colon=False): printer = _Printer( out=out, indent=indent, as_utf8=as_utf8, as_one_line=as_one_line, use_short_repeated_primitives=use_short_repeated_primitives, pointy_brackets=pointy_brackets, use_index_order=use_index_order, float_format=float_format, double_format=double_format, use_field_number=use_field_number, descriptor_pool=descriptor_pool, message_formatter=message_formatter, print_unknown_fields=print_unknown_fields, force_colon=force_colon) printer.PrintMessage(message) def PrintField(field, value, out, indent=0, as_utf8=False, as_one_line=False, use_short_repeated_primitives=False, pointy_brackets=False, use_index_order=False, float_format=None, double_format=None, message_formatter=None, print_unknown_fields=False, force_colon=False): """Print a single field name/value pair.""" printer = _Printer(out, indent, as_utf8, as_one_line, use_short_repeated_primitives, pointy_brackets, use_index_order, float_format, double_format, message_formatter=message_formatter, print_unknown_fields=print_unknown_fields, force_colon=force_colon) printer.PrintField(field, value) def PrintFieldValue(field, value, out, indent=0, as_utf8=False, as_one_line=False, use_short_repeated_primitives=False, pointy_brackets=False, use_index_order=False, float_format=None, double_format=None, message_formatter=None, print_unknown_fields=False, force_colon=False): """Print a single field value (not including name).""" printer = _Printer(out, indent, as_utf8, as_one_line, use_short_repeated_primitives, pointy_brackets, use_index_order, float_format, double_format, message_formatter=message_formatter, print_unknown_fields=print_unknown_fields, force_colon=force_colon) printer.PrintFieldValue(field, value) def _BuildMessageFromTypeName(type_name, descriptor_pool): """Returns a protobuf message instance. Args: type_name: Fully-qualified protobuf message type name string. descriptor_pool: DescriptorPool instance. Returns: A Message instance of type matching type_name, or None if the a Descriptor wasn't found matching type_name. """ # pylint: disable=g-import-not-at-top if descriptor_pool is None: from google.protobuf import descriptor_pool as pool_mod descriptor_pool = pool_mod.Default() from google.protobuf import symbol_database database = symbol_database.Default() try: message_descriptor = descriptor_pool.FindMessageTypeByName(type_name) except KeyError: return None message_type = database.GetPrototype(message_descriptor) return message_type() # These values must match WireType enum in google/protobuf/wire_format.h. WIRETYPE_LENGTH_DELIMITED = 2 WIRETYPE_START_GROUP = 3 class _Printer(object): """Text format printer for protocol message.""" def __init__( self, out, indent=0, as_utf8=False, as_one_line=False, use_short_repeated_primitives=False, pointy_brackets=False, use_index_order=False, float_format=None, double_format=None, use_field_number=False, descriptor_pool=None, message_formatter=None, print_unknown_fields=False, force_colon=False): """Initialize the Printer. Double values can be formatted compactly with 15 digits of precision (which is the most that IEEE 754 "double" can guarantee) using double_format='.15g'. To ensure that converting to text and back to a proto will result in an identical value, double_format='.17g' should be used. Args: out: To record the text format result. indent: The initial indent level for pretty print. as_utf8: Return unescaped Unicode for non-ASCII characters. In Python 3 actual Unicode characters may appear as is in strings. In Python 2 the return value will be valid UTF-8 rather than ASCII. as_one_line: Don't introduce newlines between fields. use_short_repeated_primitives: Use short repeated format for primitives. pointy_brackets: If True, use angle brackets instead of curly braces for nesting. use_index_order: If True, print fields of a proto message using the order defined in source code instead of the field number. By default, use the field number order. float_format: If set, use this to specify float field formatting (per the "Format Specification Mini-Language"); otherwise, shortest float that has same value in wire will be printed. Also affect double field if double_format is not set but float_format is set. double_format: If set, use this to specify double field formatting (per the "Format Specification Mini-Language"); if it is not set but float_format is set, use float_format. Otherwise, str() is used. use_field_number: If True, print field numbers instead of names. descriptor_pool: A DescriptorPool used to resolve Any types. message_formatter: A function(message, indent, as_one_line): unicode|None to custom format selected sub-messages (usually based on message type). Use to pretty print parts of the protobuf for easier diffing. print_unknown_fields: If True, unknown fields will be printed. force_colon: If set, a colon will be added after the field name even if the field is a proto message. """ self.out = out self.indent = indent self.as_utf8 = as_utf8 self.as_one_line = as_one_line self.use_short_repeated_primitives = use_short_repeated_primitives self.pointy_brackets = pointy_brackets self.use_index_order = use_index_order self.float_format = float_format if double_format is not None: self.double_format = double_format else: self.double_format = float_format self.use_field_number = use_field_number self.descriptor_pool = descriptor_pool self.message_formatter = message_formatter self.print_unknown_fields = print_unknown_fields self.force_colon = force_colon def _TryPrintAsAnyMessage(self, message): """Serializes if message is a google.protobuf.Any field.""" if '/' not in message.type_url: return False packed_message = _BuildMessageFromTypeName(message.TypeName(), self.descriptor_pool) if packed_message: packed_message.MergeFromString(message.value) colon = ':' if self.force_colon else '' self.out.write('%s[%s]%s ' % (self.indent * ' ', message.type_url, colon)) self._PrintMessageFieldValue(packed_message) self.out.write(' ' if self.as_one_line else '\n') return True else: return False def _TryCustomFormatMessage(self, message): formatted = self.message_formatter(message, self.indent, self.as_one_line) if formatted is None: return False out = self.out out.write(' ' * self.indent) out.write(formatted) out.write(' ' if self.as_one_line else '\n') return True def PrintMessage(self, message): """Convert protobuf message to text format. Args: message: The protocol buffers message. """ if self.message_formatter and self._TryCustomFormatMessage(message): return if (message.DESCRIPTOR.full_name == _ANY_FULL_TYPE_NAME and self._TryPrintAsAnyMessage(message)): return fields = message.ListFields() if self.use_index_order: fields.sort( key=lambda x: x[0].number if x[0].is_extension else x[0].index) for field, value in fields: if _IsMapEntry(field): for key in sorted(value): # This is slow for maps with submessage entries because it copies the # entire tree. Unfortunately this would take significant refactoring # of this file to work around. # # TODO(haberman): refactor and optimize if this becomes an issue. entry_submsg = value.GetEntryClass()(key=key, value=value[key]) self.PrintField(field, entry_submsg) elif field.label == descriptor.FieldDescriptor.LABEL_REPEATED: if (self.use_short_repeated_primitives and field.cpp_type != descriptor.FieldDescriptor.CPPTYPE_MESSAGE and field.cpp_type != descriptor.FieldDescriptor.CPPTYPE_STRING): self._PrintShortRepeatedPrimitivesValue(field, value) else: for element in value: self.PrintField(field, element) else: self.PrintField(field, value) if self.print_unknown_fields: self._PrintUnknownFields(message.UnknownFields()) def _PrintUnknownFields(self, unknown_fields): """Print unknown fields.""" out = self.out for field in unknown_fields: out.write(' ' * self.indent) out.write(str(field.field_number)) if field.wire_type == WIRETYPE_START_GROUP: if self.as_one_line: out.write(' { ') else: out.write(' {\n') self.indent += 2 self._PrintUnknownFields(field.data) if self.as_one_line: out.write('} ') else: self.indent -= 2 out.write(' ' * self.indent + '}\n') elif field.wire_type == WIRETYPE_LENGTH_DELIMITED: try: # If this field is parseable as a Message, it is probably # an embedded message. # pylint: disable=protected-access (embedded_unknown_message, pos) = decoder._DecodeUnknownFieldSet( memoryview(field.data), 0, len(field.data)) except Exception: # pylint: disable=broad-except pos = 0 if pos == len(field.data): if self.as_one_line: out.write(' { ') else: out.write(' {\n') self.indent += 2 self._PrintUnknownFields(embedded_unknown_message) if self.as_one_line: out.write('} ') else: self.indent -= 2 out.write(' ' * self.indent + '}\n') else: # A string or bytes field. self.as_utf8 may not work. out.write(': \"') out.write(text_encoding.CEscape(field.data, False)) out.write('\" ' if self.as_one_line else '\"\n') else: # varint, fixed32, fixed64 out.write(': ') out.write(str(field.data)) out.write(' ' if self.as_one_line else '\n') def _PrintFieldName(self, field): """Print field name.""" out = self.out out.write(' ' * self.indent) if self.use_field_number: out.write(str(field.number)) else: if field.is_extension: out.write('[') if (field.containing_type.GetOptions().message_set_wire_format and field.type == descriptor.FieldDescriptor.TYPE_MESSAGE and field.label == descriptor.FieldDescriptor.LABEL_OPTIONAL): out.write(field.message_type.full_name) else: out.write(field.full_name) out.write(']') elif field.type == descriptor.FieldDescriptor.TYPE_GROUP: # For groups, use the capitalized name. out.write(field.message_type.name) else: out.write(field.name) if (self.force_colon or field.cpp_type != descriptor.FieldDescriptor.CPPTYPE_MESSAGE): # The colon is optional in this case, but our cross-language golden files # don't include it. Here, the colon is only included if force_colon is # set to True out.write(':') def PrintField(self, field, value): """Print a single field name/value pair.""" self._PrintFieldName(field) self.out.write(' ') self.PrintFieldValue(field, value) self.out.write(' ' if self.as_one_line else '\n') def _PrintShortRepeatedPrimitivesValue(self, field, value): """"Prints short repeated primitives value.""" # Note: this is called only when value has at least one element. self._PrintFieldName(field) self.out.write(' [') for i in range(len(value) - 1): self.PrintFieldValue(field, value[i]) self.out.write(', ') self.PrintFieldValue(field, value[-1]) self.out.write(']') self.out.write(' ' if self.as_one_line else '\n') def _PrintMessageFieldValue(self, value): if self.pointy_brackets: openb = '<' closeb = '>' else: openb = '{' closeb = '}' if self.as_one_line: self.out.write('%s ' % openb) self.PrintMessage(value) self.out.write(closeb) else: self.out.write('%s\n' % openb) self.indent += 2 self.PrintMessage(value) self.indent -= 2 self.out.write(' ' * self.indent + closeb) def PrintFieldValue(self, field, value): """Print a single field value (not including name). For repeated fields, the value should be a single element. Args: field: The descriptor of the field to be printed. value: The value of the field. """ out = self.out if field.cpp_type == descriptor.FieldDescriptor.CPPTYPE_MESSAGE: self._PrintMessageFieldValue(value) elif field.cpp_type == descriptor.FieldDescriptor.CPPTYPE_ENUM: enum_value = field.enum_type.values_by_number.get(value, None) if enum_value is not None: out.write(enum_value.name) else: out.write(str(value)) elif field.cpp_type == descriptor.FieldDescriptor.CPPTYPE_STRING: out.write('\"') if isinstance(value, str) and not self.as_utf8: out_value = value.encode('utf-8') else: out_value = value if field.type == descriptor.FieldDescriptor.TYPE_BYTES: # We always need to escape all binary data in TYPE_BYTES fields. out_as_utf8 = False else: out_as_utf8 = self.as_utf8 out.write(text_encoding.CEscape(out_value, out_as_utf8)) out.write('\"') elif field.cpp_type == descriptor.FieldDescriptor.CPPTYPE_BOOL: if value: out.write('true') else: out.write('false') elif field.cpp_type == descriptor.FieldDescriptor.CPPTYPE_FLOAT: if self.float_format is not None: out.write('{1:{0}}'.format(self.float_format, value)) else: if math.isnan(value): out.write(str(value)) else: out.write(str(type_checkers.ToShortestFloat(value))) elif (field.cpp_type == descriptor.FieldDescriptor.CPPTYPE_DOUBLE and self.double_format is not None): out.write('{1:{0}}'.format(self.double_format, value)) else: out.write(str(value)) def Parse(text, message, allow_unknown_extension=False, allow_field_number=False, descriptor_pool=None, allow_unknown_field=False): """Parses a text representation of a protocol message into a message. NOTE: for historical reasons this function does not clear the input message. This is different from what the binary msg.ParseFrom(...) does. If text contains a field already set in message, the value is appended if the field is repeated. Otherwise, an error is raised. Example:: a = MyProto() a.repeated_field.append('test') b = MyProto() # Repeated fields are combined text_format.Parse(repr(a), b) text_format.Parse(repr(a), b) # repeated_field contains ["test", "test"] # Non-repeated fields cannot be overwritten a.singular_field = 1 b.singular_field = 2 text_format.Parse(repr(a), b) # ParseError # Binary version: b.ParseFromString(a.SerializeToString()) # repeated_field is now "test" Caller is responsible for clearing the message as needed. Args: text (str): Message text representation. message (Message): A protocol buffer message to merge into. allow_unknown_extension: if True, skip over missing extensions and keep parsing allow_field_number: if True, both field number and field name are allowed. descriptor_pool (DescriptorPool): Descriptor pool used to resolve Any types. allow_unknown_field: if True, skip over unknown field and keep parsing. Avoid to use this option if possible. It may hide some errors (e.g. spelling error on field name) Returns: Message: The same message passed as argument. Raises: ParseError: On text parsing problems. """ return ParseLines(text.split(b'\n' if isinstance(text, bytes) else u'\n'), message, allow_unknown_extension, allow_field_number, descriptor_pool=descriptor_pool, allow_unknown_field=allow_unknown_field) def Merge(text, message, allow_unknown_extension=False, allow_field_number=False, descriptor_pool=None, allow_unknown_field=False): """Parses a text representation of a protocol message into a message. Like Parse(), but allows repeated values for a non-repeated field, and uses the last one. This means any non-repeated, top-level fields specified in text replace those in the message. Args: text (str): Message text representation. message (Message): A protocol buffer message to merge into. allow_unknown_extension: if True, skip over missing extensions and keep parsing allow_field_number: if True, both field number and field name are allowed. descriptor_pool (DescriptorPool): Descriptor pool used to resolve Any types. allow_unknown_field: if True, skip over unknown field and keep parsing. Avoid to use this option if possible. It may hide some errors (e.g. spelling error on field name) Returns: Message: The same message passed as argument. Raises: ParseError: On text parsing problems. """ return MergeLines( text.split(b'\n' if isinstance(text, bytes) else u'\n'), message, allow_unknown_extension, allow_field_number, descriptor_pool=descriptor_pool, allow_unknown_field=allow_unknown_field) def ParseLines(lines, message, allow_unknown_extension=False, allow_field_number=False, descriptor_pool=None, allow_unknown_field=False): """Parses a text representation of a protocol message into a message. See Parse() for caveats. Args: lines: An iterable of lines of a message's text representation. message: A protocol buffer message to merge into. allow_unknown_extension: if True, skip over missing extensions and keep parsing allow_field_number: if True, both field number and field name are allowed. descriptor_pool: A DescriptorPool used to resolve Any types. allow_unknown_field: if True, skip over unknown field and keep parsing. Avoid to use this option if possible. It may hide some errors (e.g. spelling error on field name) Returns: The same message passed as argument. Raises: ParseError: On text parsing problems. """ parser = _Parser(allow_unknown_extension, allow_field_number, descriptor_pool=descriptor_pool, allow_unknown_field=allow_unknown_field) return parser.ParseLines(lines, message) def MergeLines(lines, message, allow_unknown_extension=False, allow_field_number=False, descriptor_pool=None, allow_unknown_field=False): """Parses a text representation of a protocol message into a message. See Merge() for more details. Args: lines: An iterable of lines of a message's text representation. message: A protocol buffer message to merge into. allow_unknown_extension: if True, skip over missing extensions and keep parsing allow_field_number: if True, both field number and field name are allowed. descriptor_pool: A DescriptorPool used to resolve Any types. allow_unknown_field: if True, skip over unknown field and keep parsing. Avoid to use this option if possible. It may hide some errors (e.g. spelling error on field name) Returns: The same message passed as argument. Raises: ParseError: On text parsing problems. """ parser = _Parser(allow_unknown_extension, allow_field_number, descriptor_pool=descriptor_pool, allow_unknown_field=allow_unknown_field) return parser.MergeLines(lines, message) class _Parser(object): """Text format parser for protocol message.""" def __init__(self, allow_unknown_extension=False, allow_field_number=False, descriptor_pool=None, allow_unknown_field=False): self.allow_unknown_extension = allow_unknown_extension self.allow_field_number = allow_field_number self.descriptor_pool = descriptor_pool self.allow_unknown_field = allow_unknown_field def ParseLines(self, lines, message): """Parses a text representation of a protocol message into a message.""" self._allow_multiple_scalars = False self._ParseOrMerge(lines, message) return message def MergeLines(self, lines, message): """Merges a text representation of a protocol message into a message.""" self._allow_multiple_scalars = True self._ParseOrMerge(lines, message) return message def _ParseOrMerge(self, lines, message): """Converts a text representation of a protocol message into a message. Args: lines: Lines of a message's text representation. message: A protocol buffer message to merge into. Raises: ParseError: On text parsing problems. """ # Tokenize expects native str lines. str_lines = ( line if isinstance(line, str) else line.decode('utf-8') for line in lines) tokenizer = Tokenizer(str_lines) while not tokenizer.AtEnd(): self._MergeField(tokenizer, message) def _MergeField(self, tokenizer, message): """Merges a single protocol message field into a message. Args: tokenizer: A tokenizer to parse the field name and values. message: A protocol message to record the data. Raises: ParseError: In case of text parsing problems. """ message_descriptor = message.DESCRIPTOR if (message_descriptor.full_name == _ANY_FULL_TYPE_NAME and tokenizer.TryConsume('[')): type_url_prefix, packed_type_name = self._ConsumeAnyTypeUrl(tokenizer) tokenizer.Consume(']') tokenizer.TryConsume(':') if tokenizer.TryConsume('<'): expanded_any_end_token = '>' else: tokenizer.Consume('{') expanded_any_end_token = '}' expanded_any_sub_message = _BuildMessageFromTypeName(packed_type_name, self.descriptor_pool) if not expanded_any_sub_message: raise ParseError('Type %s not found in descriptor pool' % packed_type_name) while not tokenizer.TryConsume(expanded_any_end_token): if tokenizer.AtEnd(): raise tokenizer.ParseErrorPreviousToken('Expected "%s".' % (expanded_any_end_token,)) self._MergeField(tokenizer, expanded_any_sub_message) deterministic = False message.Pack(expanded_any_sub_message, type_url_prefix=type_url_prefix, deterministic=deterministic) return if tokenizer.TryConsume('['): name = [tokenizer.ConsumeIdentifier()] while tokenizer.TryConsume('.'): name.append(tokenizer.ConsumeIdentifier()) name = '.'.join(name) if not message_descriptor.is_extendable: raise tokenizer.ParseErrorPreviousToken( 'Message type "%s" does not have extensions.' % message_descriptor.full_name) # pylint: disable=protected-access field = message.Extensions._FindExtensionByName(name) # pylint: enable=protected-access if not field: if self.allow_unknown_extension: field = None else: raise tokenizer.ParseErrorPreviousToken( 'Extension "%s" not registered. ' 'Did you import the _pb2 module which defines it? ' 'If you are trying to place the extension in the MessageSet ' 'field of another message that is in an Any or MessageSet field, ' 'that message\'s _pb2 module must be imported as well' % name) elif message_descriptor != field.containing_type: raise tokenizer.ParseErrorPreviousToken( 'Extension "%s" does not extend message type "%s".' % (name, message_descriptor.full_name)) tokenizer.Consume(']') else: name = tokenizer.ConsumeIdentifierOrNumber() if self.allow_field_number and name.isdigit(): number = ParseInteger(name, True, True) field = message_descriptor.fields_by_number.get(number, None) if not field and message_descriptor.is_extendable: field = message.Extensions._FindExtensionByNumber(number) else: field = message_descriptor.fields_by_name.get(name, None) # Group names are expected to be capitalized as they appear in the # .proto file, which actually matches their type names, not their field # names. if not field: field = message_descriptor.fields_by_name.get(name.lower(), None) if field and field.type != descriptor.FieldDescriptor.TYPE_GROUP: field = None if (field and field.type == descriptor.FieldDescriptor.TYPE_GROUP and field.message_type.name != name): field = None if not field and not self.allow_unknown_field: raise tokenizer.ParseErrorPreviousToken( 'Message type "%s" has no field named "%s".' % (message_descriptor.full_name, name)) if field: if not self._allow_multiple_scalars and field.containing_oneof: # Check if there's a different field set in this oneof. # Note that we ignore the case if the same field was set before, and we # apply _allow_multiple_scalars to non-scalar fields as well. which_oneof = message.WhichOneof(field.containing_oneof.name) if which_oneof is not None and which_oneof != field.name: raise tokenizer.ParseErrorPreviousToken( 'Field "%s" is specified along with field "%s", another member ' 'of oneof "%s" for message type "%s".' % (field.name, which_oneof, field.containing_oneof.name, message_descriptor.full_name)) if field.cpp_type == descriptor.FieldDescriptor.CPPTYPE_MESSAGE: tokenizer.TryConsume(':') merger = self._MergeMessageField else: tokenizer.Consume(':') merger = self._MergeScalarField if (field.label == descriptor.FieldDescriptor.LABEL_REPEATED and tokenizer.TryConsume('[')): # Short repeated format, e.g. "foo: [1, 2, 3]" if not tokenizer.TryConsume(']'): while True: merger(tokenizer, message, field) if tokenizer.TryConsume(']'): break tokenizer.Consume(',') else: merger(tokenizer, message, field) else: # Proto field is unknown. assert (self.allow_unknown_extension or self.allow_unknown_field) _SkipFieldContents(tokenizer) # For historical reasons, fields may optionally be separated by commas or # semicolons. if not tokenizer.TryConsume(','): tokenizer.TryConsume(';') def _ConsumeAnyTypeUrl(self, tokenizer): """Consumes a google.protobuf.Any type URL and returns the type name.""" # Consume "type.googleapis.com/". prefix = [tokenizer.ConsumeIdentifier()] tokenizer.Consume('.') prefix.append(tokenizer.ConsumeIdentifier()) tokenizer.Consume('.') prefix.append(tokenizer.ConsumeIdentifier()) tokenizer.Consume('/') # Consume the fully-qualified type name. name = [tokenizer.ConsumeIdentifier()] while tokenizer.TryConsume('.'): name.append(tokenizer.ConsumeIdentifier()) return '.'.join(prefix), '.'.join(name) def _MergeMessageField(self, tokenizer, message, field): """Merges a single scalar field into a message. Args: tokenizer: A tokenizer to parse the field value. message: The message of which field is a member. field: The descriptor of the field to be merged. Raises: ParseError: In case of text parsing problems. """ is_map_entry = _IsMapEntry(field) if tokenizer.TryConsume('<'): end_token = '>' else: tokenizer.Consume('{') end_token = '}' if field.label == descriptor.FieldDescriptor.LABEL_REPEATED: if field.is_extension: sub_message = message.Extensions[field].add() elif is_map_entry: sub_message = getattr(message, field.name).GetEntryClass()() else: sub_message = getattr(message, field.name).add() else: if field.is_extension: if (not self._allow_multiple_scalars and message.HasExtension(field)): raise tokenizer.ParseErrorPreviousToken( 'Message type "%s" should not have multiple "%s" extensions.' % (message.DESCRIPTOR.full_name, field.full_name)) sub_message = message.Extensions[field] else: # Also apply _allow_multiple_scalars to message field. # TODO(jieluo): Change to _allow_singular_overwrites. if (not self._allow_multiple_scalars and message.HasField(field.name)): raise tokenizer.ParseErrorPreviousToken( 'Message type "%s" should not have multiple "%s" fields.' % (message.DESCRIPTOR.full_name, field.name)) sub_message = getattr(message, field.name) sub_message.SetInParent() while not tokenizer.TryConsume(end_token): if tokenizer.AtEnd(): raise tokenizer.ParseErrorPreviousToken('Expected "%s".' % (end_token,)) self._MergeField(tokenizer, sub_message) if is_map_entry: value_cpptype = field.message_type.fields_by_name['value'].cpp_type if value_cpptype == descriptor.FieldDescriptor.CPPTYPE_MESSAGE: value = getattr(message, field.name)[sub_message.key] value.CopyFrom(sub_message.value) else: getattr(message, field.name)[sub_message.key] = sub_message.value @staticmethod def _IsProto3Syntax(message): message_descriptor = message.DESCRIPTOR return (hasattr(message_descriptor, 'syntax') and message_descriptor.syntax == 'proto3') def _MergeScalarField(self, tokenizer, message, field): """Merges a single scalar field into a message. Args: tokenizer: A tokenizer to parse the field value. message: A protocol message to record the data. field: The descriptor of the field to be merged. Raises: ParseError: In case of text parsing problems. RuntimeError: On runtime errors. """ _ = self.allow_unknown_extension value = None if field.type in (descriptor.FieldDescriptor.TYPE_INT32, descriptor.FieldDescriptor.TYPE_SINT32, descriptor.FieldDescriptor.TYPE_SFIXED32): value = _ConsumeInt32(tokenizer) elif field.type in (descriptor.FieldDescriptor.TYPE_INT64, descriptor.FieldDescriptor.TYPE_SINT64, descriptor.FieldDescriptor.TYPE_SFIXED64): value = _ConsumeInt64(tokenizer) elif field.type in (descriptor.FieldDescriptor.TYPE_UINT32, descriptor.FieldDescriptor.TYPE_FIXED32): value = _ConsumeUint32(tokenizer) elif field.type in (descriptor.FieldDescriptor.TYPE_UINT64, descriptor.FieldDescriptor.TYPE_FIXED64): value = _ConsumeUint64(tokenizer) elif field.type in (descriptor.FieldDescriptor.TYPE_FLOAT, descriptor.FieldDescriptor.TYPE_DOUBLE): value = tokenizer.ConsumeFloat() elif field.type == descriptor.FieldDescriptor.TYPE_BOOL: value = tokenizer.ConsumeBool() elif field.type == descriptor.FieldDescriptor.TYPE_STRING: value = tokenizer.ConsumeString() elif field.type == descriptor.FieldDescriptor.TYPE_BYTES: value = tokenizer.ConsumeByteString() elif field.type == descriptor.FieldDescriptor.TYPE_ENUM: value = tokenizer.ConsumeEnum(field) else: raise RuntimeError('Unknown field type %d' % field.type) if field.label == descriptor.FieldDescriptor.LABEL_REPEATED: if field.is_extension: message.Extensions[field].append(value) else: getattr(message, field.name).append(value) else: if field.is_extension: if (not self._allow_multiple_scalars and not self._IsProto3Syntax(message) and message.HasExtension(field)): raise tokenizer.ParseErrorPreviousToken( 'Message type "%s" should not have multiple "%s" extensions.' % (message.DESCRIPTOR.full_name, field.full_name)) else: message.Extensions[field] = value else: duplicate_error = False if not self._allow_multiple_scalars: if self._IsProto3Syntax(message): # Proto3 doesn't represent presence so we try best effort to check # multiple scalars by compare to default values. duplicate_error = bool(getattr(message, field.name)) else: duplicate_error = message.HasField(field.name) if duplicate_error: raise tokenizer.ParseErrorPreviousToken( 'Message type "%s" should not have multiple "%s" fields.' % (message.DESCRIPTOR.full_name, field.name)) else: setattr(message, field.name, value) def _SkipFieldContents(tokenizer): """Skips over contents (value or message) of a field. Args: tokenizer: A tokenizer to parse the field name and values. """ # Try to guess the type of this field. # If this field is not a message, there should be a ":" between the # field name and the field value and also the field value should not # start with "{" or "<" which indicates the beginning of a message body. # If there is no ":" or there is a "{" or "<" after ":", this field has # to be a message or the input is ill-formed. if tokenizer.TryConsume(':') and not tokenizer.LookingAt( '{') and not tokenizer.LookingAt('<'): _SkipFieldValue(tokenizer) else: _SkipFieldMessage(tokenizer) def _SkipField(tokenizer): """Skips over a complete field (name and value/message). Args: tokenizer: A tokenizer to parse the field name and values. """ if tokenizer.TryConsume('['): # Consume extension name. tokenizer.ConsumeIdentifier() while tokenizer.TryConsume('.'): tokenizer.ConsumeIdentifier() tokenizer.Consume(']') else: tokenizer.ConsumeIdentifierOrNumber() _SkipFieldContents(tokenizer) # For historical reasons, fields may optionally be separated by commas or # semicolons. if not tokenizer.TryConsume(','): tokenizer.TryConsume(';') def _SkipFieldMessage(tokenizer): """Skips over a field message. Args: tokenizer: A tokenizer to parse the field name and values. """ if tokenizer.TryConsume('<'): delimiter = '>' else: tokenizer.Consume('{') delimiter = '}' while not tokenizer.LookingAt('>') and not tokenizer.LookingAt('}'): _SkipField(tokenizer) tokenizer.Consume(delimiter) def _SkipFieldValue(tokenizer): """Skips over a field value. Args: tokenizer: A tokenizer to parse the field name and values. Raises: ParseError: In case an invalid field value is found. """ # String/bytes tokens can come in multiple adjacent string literals. # If we can consume one, consume as many as we can. if tokenizer.TryConsumeByteString(): while tokenizer.TryConsumeByteString(): pass return if (not tokenizer.TryConsumeIdentifier() and not _TryConsumeInt64(tokenizer) and not _TryConsumeUint64(tokenizer) and not tokenizer.TryConsumeFloat()): raise ParseError('Invalid field value: ' + tokenizer.token) class Tokenizer(object): """Protocol buffer text representation tokenizer. This class handles the lower level string parsing by splitting it into meaningful tokens. It was directly ported from the Java protocol buffer API. """ _WHITESPACE = re.compile(r'\s+') _COMMENT = re.compile(r'(\s*#.*$)', re.MULTILINE) _WHITESPACE_OR_COMMENT = re.compile(r'(\s|(#.*$))+', re.MULTILINE) _TOKEN = re.compile('|'.join([ r'[a-zA-Z_][0-9a-zA-Z_+-]*', # an identifier r'([0-9+-]|(\.[0-9]))[0-9a-zA-Z_.+-]*', # a number ] + [ # quoted str for each quote mark # Avoid backtracking! https://stackoverflow.com/a/844267 r'{qt}[^{qt}\n\\]*((\\.)+[^{qt}\n\\]*)*({qt}|\\?$)'.format(qt=mark) for mark in _QUOTES ])) _IDENTIFIER = re.compile(r'[^\d\W]\w*') _IDENTIFIER_OR_NUMBER = re.compile(r'\w+') def __init__(self, lines, skip_comments=True): self._position = 0 self._line = -1 self._column = 0 self._token_start = None self.token = '' self._lines = iter(lines) self._current_line = '' self._previous_line = 0 self._previous_column = 0 self._more_lines = True self._skip_comments = skip_comments self._whitespace_pattern = (skip_comments and self._WHITESPACE_OR_COMMENT or self._WHITESPACE) self._SkipWhitespace() self.NextToken() def LookingAt(self, token): return self.token == token def AtEnd(self): """Checks the end of the text was reached. Returns: True iff the end was reached. """ return not self.token def _PopLine(self): while len(self._current_line) <= self._column: try: self._current_line = next(self._lines) except StopIteration: self._current_line = '' self._more_lines = False return else: self._line += 1 self._column = 0 def _SkipWhitespace(self): while True: self._PopLine() match = self._whitespace_pattern.match(self._current_line, self._column) if not match: break length = len(match.group(0)) self._column += length def TryConsume(self, token): """Tries to consume a given piece of text. Args: token: Text to consume. Returns: True iff the text was consumed. """ if self.token == token: self.NextToken() return True return False def Consume(self, token): """Consumes a piece of text. Args: token: Text to consume. Raises: ParseError: If the text couldn't be consumed. """ if not self.TryConsume(token): raise self.ParseError('Expected "%s".' % token) def ConsumeComment(self): result = self.token if not self._COMMENT.match(result): raise self.ParseError('Expected comment.') self.NextToken() return result def ConsumeCommentOrTrailingComment(self): """Consumes a comment, returns a 2-tuple (trailing bool, comment str).""" # Tokenizer initializes _previous_line and _previous_column to 0. As the # tokenizer starts, it looks like there is a previous token on the line. just_started = self._line == 0 and self._column == 0 before_parsing = self._previous_line comment = self.ConsumeComment() # A trailing comment is a comment on the same line than the previous token. trailing = (self._previous_line == before_parsing and not just_started) return trailing, comment def TryConsumeIdentifier(self): try: self.ConsumeIdentifier() return True except ParseError: return False def ConsumeIdentifier(self): """Consumes protocol message field identifier. Returns: Identifier string. Raises: ParseError: If an identifier couldn't be consumed. """ result = self.token if not self._IDENTIFIER.match(result): raise self.ParseError('Expected identifier.') self.NextToken() return result def TryConsumeIdentifierOrNumber(self): try: self.ConsumeIdentifierOrNumber() return True except ParseError: return False def ConsumeIdentifierOrNumber(self): """Consumes protocol message field identifier. Returns: Identifier string. Raises: ParseError: If an identifier couldn't be consumed. """ result = self.token if not self._IDENTIFIER_OR_NUMBER.match(result): raise self.ParseError('Expected identifier or number, got %s.' % result) self.NextToken() return result def TryConsumeInteger(self): try: self.ConsumeInteger() return True except ParseError: return False def ConsumeInteger(self): """Consumes an integer number. Returns: The integer parsed. Raises: ParseError: If an integer couldn't be consumed. """ try: result = _ParseAbstractInteger(self.token) except ValueError as e: raise self.ParseError(str(e)) self.NextToken() return result def TryConsumeFloat(self): try: self.ConsumeFloat() return True except ParseError: return False def ConsumeFloat(self): """Consumes an floating point number. Returns: The number parsed. Raises: ParseError: If a floating point number couldn't be consumed. """ try: result = ParseFloat(self.token) except ValueError as e: raise self.ParseError(str(e)) self.NextToken() return result def ConsumeBool(self): """Consumes a boolean value. Returns: The bool parsed. Raises: ParseError: If a boolean value couldn't be consumed. """ try: result = ParseBool(self.token) except ValueError as e: raise self.ParseError(str(e)) self.NextToken() return result def TryConsumeByteString(self): try: self.ConsumeByteString() return True except ParseError: return False def ConsumeString(self): """Consumes a string value. Returns: The string parsed. Raises: ParseError: If a string value couldn't be consumed. """ the_bytes = self.ConsumeByteString() try: return str(the_bytes, 'utf-8') except UnicodeDecodeError as e: raise self._StringParseError(e) def ConsumeByteString(self): """Consumes a byte array value. Returns: The array parsed (as a string). Raises: ParseError: If a byte array value couldn't be consumed. """ the_list = [self._ConsumeSingleByteString()] while self.token and self.token[0] in _QUOTES: the_list.append(self._ConsumeSingleByteString()) return b''.join(the_list) def _ConsumeSingleByteString(self): """Consume one token of a string literal. String literals (whether bytes or text) can come in multiple adjacent tokens which are automatically concatenated, like in C or Python. This method only consumes one token. Returns: The token parsed. Raises: ParseError: When the wrong format data is found. """ text = self.token if len(text) < 1 or text[0] not in _QUOTES: raise self.ParseError('Expected string but found: %r' % (text,)) if len(text) < 2 or text[-1] != text[0]: raise self.ParseError('String missing ending quote: %r' % (text,)) try: result = text_encoding.CUnescape(text[1:-1]) except ValueError as e: raise self.ParseError(str(e)) self.NextToken() return result def ConsumeEnum(self, field): try: result = ParseEnum(field, self.token) except ValueError as e: raise self.ParseError(str(e)) self.NextToken() return result def ParseErrorPreviousToken(self, message): """Creates and *returns* a ParseError for the previously read token. Args: message: A message to set for the exception. Returns: A ParseError instance. """ return ParseError(message, self._previous_line + 1, self._previous_column + 1) def ParseError(self, message): """Creates and *returns* a ParseError for the current token.""" return ParseError('\'' + self._current_line + '\': ' + message, self._line + 1, self._column + 1) def _StringParseError(self, e): return self.ParseError('Couldn\'t parse string: ' + str(e)) def NextToken(self): """Reads the next meaningful token.""" self._previous_line = self._line self._previous_column = self._column self._column += len(self.token) self._SkipWhitespace() if not self._more_lines: self.token = '' return match = self._TOKEN.match(self._current_line, self._column) if not match and not self._skip_comments: match = self._COMMENT.match(self._current_line, self._column) if match: token = match.group(0) self.token = token else: self.token = self._current_line[self._column] # Aliased so it can still be accessed by current visibility violators. # TODO(dbarnett): Migrate violators to textformat_tokenizer. _Tokenizer = Tokenizer # pylint: disable=invalid-name def _ConsumeInt32(tokenizer): """Consumes a signed 32bit integer number from tokenizer. Args: tokenizer: A tokenizer used to parse the number. Returns: The integer parsed. Raises: ParseError: If a signed 32bit integer couldn't be consumed. """ return _ConsumeInteger(tokenizer, is_signed=True, is_long=False) def _ConsumeUint32(tokenizer): """Consumes an unsigned 32bit integer number from tokenizer. Args: tokenizer: A tokenizer used to parse the number. Returns: The integer parsed. Raises: ParseError: If an unsigned 32bit integer couldn't be consumed. """ return _ConsumeInteger(tokenizer, is_signed=False, is_long=False) def _TryConsumeInt64(tokenizer): try: _ConsumeInt64(tokenizer) return True except ParseError: return False def _ConsumeInt64(tokenizer): """Consumes a signed 32bit integer number from tokenizer. Args: tokenizer: A tokenizer used to parse the number. Returns: The integer parsed. Raises: ParseError: If a signed 32bit integer couldn't be consumed. """ return _ConsumeInteger(tokenizer, is_signed=True, is_long=True) def _TryConsumeUint64(tokenizer): try: _ConsumeUint64(tokenizer) return True except ParseError: return False def _ConsumeUint64(tokenizer): """Consumes an unsigned 64bit integer number from tokenizer. Args: tokenizer: A tokenizer used to parse the number. Returns: The integer parsed. Raises: ParseError: If an unsigned 64bit integer couldn't be consumed. """ return _ConsumeInteger(tokenizer, is_signed=False, is_long=True) def _ConsumeInteger(tokenizer, is_signed=False, is_long=False): """Consumes an integer number from tokenizer. Args: tokenizer: A tokenizer used to parse the number. is_signed: True if a signed integer must be parsed. is_long: True if a long integer must be parsed. Returns: The integer parsed. Raises: ParseError: If an integer with given characteristics couldn't be consumed. """ try: result = ParseInteger(tokenizer.token, is_signed=is_signed, is_long=is_long) except ValueError as e: raise tokenizer.ParseError(str(e)) tokenizer.NextToken() return result def ParseInteger(text, is_signed=False, is_long=False): """Parses an integer. Args: text: The text to parse. is_signed: True if a signed integer must be parsed. is_long: True if a long integer must be parsed. Returns: The integer value. Raises: ValueError: Thrown Iff the text is not a valid integer. """ # Do the actual parsing. Exception handling is propagated to caller. result = _ParseAbstractInteger(text) # Check if the integer is sane. Exceptions handled by callers. checker = _INTEGER_CHECKERS[2 * int(is_long) + int(is_signed)] checker.CheckValue(result) return result def _ParseAbstractInteger(text): """Parses an integer without checking size/signedness. Args: text: The text to parse. Returns: The integer value. Raises: ValueError: Thrown Iff the text is not a valid integer. """ # Do the actual parsing. Exception handling is propagated to caller. orig_text = text c_octal_match = re.match(r'(-?)0(\d+)$', text) if c_octal_match: # Python 3 no longer supports 0755 octal syntax without the 'o', so # we always use the '0o' prefix for multi-digit numbers starting with 0. text = c_octal_match.group(1) + '0o' + c_octal_match.group(2) try: return int(text, 0) except ValueError: raise ValueError('Couldn\'t parse integer: %s' % orig_text) def ParseFloat(text): """Parse a floating point number. Args: text: Text to parse. Returns: The number parsed. Raises: ValueError: If a floating point number couldn't be parsed. """ try: # Assume Python compatible syntax. return float(text) except ValueError: # Check alternative spellings. if _FLOAT_INFINITY.match(text): if text[0] == '-': return float('-inf') else: return float('inf') elif _FLOAT_NAN.match(text): return float('nan') else: # assume '1.0f' format try: return float(text.rstrip('f')) except ValueError: raise ValueError('Couldn\'t parse float: %s' % text) def ParseBool(text): """Parse a boolean value. Args: text: Text to parse. Returns: Boolean values parsed Raises: ValueError: If text is not a valid boolean. """ if text in ('true', 't', '1', 'True'): return True elif text in ('false', 'f', '0', 'False'): return False else: raise ValueError('Expected "true" or "false".') def ParseEnum(field, value): """Parse an enum value. The value can be specified by a number (the enum value), or by a string literal (the enum name). Args: field: Enum field descriptor. value: String value. Returns: Enum value number. Raises: ValueError: If the enum value could not be parsed. """ enum_descriptor = field.enum_type try: number = int(value, 0) except ValueError: # Identifier. enum_value = enum_descriptor.values_by_name.get(value, None) if enum_value is None: raise ValueError('Enum type "%s" has no value named %s.' % (enum_descriptor.full_name, value)) else: # Numeric value. if hasattr(field.file, 'syntax'): # Attribute is checked for compatibility. if field.file.syntax == 'proto3': # Proto3 accept numeric unknown enums. return number enum_value = enum_descriptor.values_by_number.get(number, None) if enum_value is None: raise ValueError('Enum type "%s" has no value with number %d.' % (enum_descriptor.full_name, number)) return enum_value.number ================================================ FILE: openpype/hosts/hiero/vendor/google/protobuf/timestamp_pb2.py ================================================ # -*- coding: utf-8 -*- # Generated by the protocol buffer compiler. DO NOT EDIT! # source: google/protobuf/timestamp.proto """Generated protocol buffer code.""" from google.protobuf.internal import builder as _builder from google.protobuf import descriptor as _descriptor from google.protobuf import descriptor_pool as _descriptor_pool from google.protobuf import symbol_database as _symbol_database # @@protoc_insertion_point(imports) _sym_db = _symbol_database.Default() DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x1fgoogle/protobuf/timestamp.proto\x12\x0fgoogle.protobuf\"+\n\tTimestamp\x12\x0f\n\x07seconds\x18\x01 \x01(\x03\x12\r\n\x05nanos\x18\x02 \x01(\x05\x42\x85\x01\n\x13\x63om.google.protobufB\x0eTimestampProtoP\x01Z2google.golang.org/protobuf/types/known/timestamppb\xf8\x01\x01\xa2\x02\x03GPB\xaa\x02\x1eGoogle.Protobuf.WellKnownTypesb\x06proto3') _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals()) _builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'google.protobuf.timestamp_pb2', globals()) if _descriptor._USE_C_DESCRIPTORS == False: DESCRIPTOR._options = None DESCRIPTOR._serialized_options = b'\n\023com.google.protobufB\016TimestampProtoP\001Z2google.golang.org/protobuf/types/known/timestamppb\370\001\001\242\002\003GPB\252\002\036Google.Protobuf.WellKnownTypes' _TIMESTAMP._serialized_start=52 _TIMESTAMP._serialized_end=95 # @@protoc_insertion_point(module_scope) ================================================ FILE: openpype/hosts/hiero/vendor/google/protobuf/type_pb2.py ================================================ # -*- coding: utf-8 -*- # Generated by the protocol buffer compiler. DO NOT EDIT! # source: google/protobuf/type.proto """Generated protocol buffer code.""" from google.protobuf.internal import builder as _builder from google.protobuf import descriptor as _descriptor from google.protobuf import descriptor_pool as _descriptor_pool from google.protobuf import symbol_database as _symbol_database # @@protoc_insertion_point(imports) _sym_db = _symbol_database.Default() from google.protobuf import any_pb2 as google_dot_protobuf_dot_any__pb2 from google.protobuf import source_context_pb2 as google_dot_protobuf_dot_source__context__pb2 DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x1agoogle/protobuf/type.proto\x12\x0fgoogle.protobuf\x1a\x19google/protobuf/any.proto\x1a$google/protobuf/source_context.proto\"\xd7\x01\n\x04Type\x12\x0c\n\x04name\x18\x01 \x01(\t\x12&\n\x06\x66ields\x18\x02 \x03(\x0b\x32\x16.google.protobuf.Field\x12\x0e\n\x06oneofs\x18\x03 \x03(\t\x12(\n\x07options\x18\x04 \x03(\x0b\x32\x17.google.protobuf.Option\x12\x36\n\x0esource_context\x18\x05 \x01(\x0b\x32\x1e.google.protobuf.SourceContext\x12\'\n\x06syntax\x18\x06 \x01(\x0e\x32\x17.google.protobuf.Syntax\"\xd5\x05\n\x05\x46ield\x12)\n\x04kind\x18\x01 \x01(\x0e\x32\x1b.google.protobuf.Field.Kind\x12\x37\n\x0b\x63\x61rdinality\x18\x02 \x01(\x0e\x32\".google.protobuf.Field.Cardinality\x12\x0e\n\x06number\x18\x03 \x01(\x05\x12\x0c\n\x04name\x18\x04 \x01(\t\x12\x10\n\x08type_url\x18\x06 \x01(\t\x12\x13\n\x0boneof_index\x18\x07 \x01(\x05\x12\x0e\n\x06packed\x18\x08 \x01(\x08\x12(\n\x07options\x18\t \x03(\x0b\x32\x17.google.protobuf.Option\x12\x11\n\tjson_name\x18\n \x01(\t\x12\x15\n\rdefault_value\x18\x0b \x01(\t\"\xc8\x02\n\x04Kind\x12\x10\n\x0cTYPE_UNKNOWN\x10\x00\x12\x0f\n\x0bTYPE_DOUBLE\x10\x01\x12\x0e\n\nTYPE_FLOAT\x10\x02\x12\x0e\n\nTYPE_INT64\x10\x03\x12\x0f\n\x0bTYPE_UINT64\x10\x04\x12\x0e\n\nTYPE_INT32\x10\x05\x12\x10\n\x0cTYPE_FIXED64\x10\x06\x12\x10\n\x0cTYPE_FIXED32\x10\x07\x12\r\n\tTYPE_BOOL\x10\x08\x12\x0f\n\x0bTYPE_STRING\x10\t\x12\x0e\n\nTYPE_GROUP\x10\n\x12\x10\n\x0cTYPE_MESSAGE\x10\x0b\x12\x0e\n\nTYPE_BYTES\x10\x0c\x12\x0f\n\x0bTYPE_UINT32\x10\r\x12\r\n\tTYPE_ENUM\x10\x0e\x12\x11\n\rTYPE_SFIXED32\x10\x0f\x12\x11\n\rTYPE_SFIXED64\x10\x10\x12\x0f\n\x0bTYPE_SINT32\x10\x11\x12\x0f\n\x0bTYPE_SINT64\x10\x12\"t\n\x0b\x43\x61rdinality\x12\x17\n\x13\x43\x41RDINALITY_UNKNOWN\x10\x00\x12\x18\n\x14\x43\x41RDINALITY_OPTIONAL\x10\x01\x12\x18\n\x14\x43\x41RDINALITY_REQUIRED\x10\x02\x12\x18\n\x14\x43\x41RDINALITY_REPEATED\x10\x03\"\xce\x01\n\x04\x45num\x12\x0c\n\x04name\x18\x01 \x01(\t\x12-\n\tenumvalue\x18\x02 \x03(\x0b\x32\x1a.google.protobuf.EnumValue\x12(\n\x07options\x18\x03 \x03(\x0b\x32\x17.google.protobuf.Option\x12\x36\n\x0esource_context\x18\x04 \x01(\x0b\x32\x1e.google.protobuf.SourceContext\x12\'\n\x06syntax\x18\x05 \x01(\x0e\x32\x17.google.protobuf.Syntax\"S\n\tEnumValue\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0e\n\x06number\x18\x02 \x01(\x05\x12(\n\x07options\x18\x03 \x03(\x0b\x32\x17.google.protobuf.Option\";\n\x06Option\x12\x0c\n\x04name\x18\x01 \x01(\t\x12#\n\x05value\x18\x02 \x01(\x0b\x32\x14.google.protobuf.Any*.\n\x06Syntax\x12\x11\n\rSYNTAX_PROTO2\x10\x00\x12\x11\n\rSYNTAX_PROTO3\x10\x01\x42{\n\x13\x63om.google.protobufB\tTypeProtoP\x01Z-google.golang.org/protobuf/types/known/typepb\xf8\x01\x01\xa2\x02\x03GPB\xaa\x02\x1eGoogle.Protobuf.WellKnownTypesb\x06proto3') _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals()) _builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'google.protobuf.type_pb2', globals()) if _descriptor._USE_C_DESCRIPTORS == False: DESCRIPTOR._options = None DESCRIPTOR._serialized_options = b'\n\023com.google.protobufB\tTypeProtoP\001Z-google.golang.org/protobuf/types/known/typepb\370\001\001\242\002\003GPB\252\002\036Google.Protobuf.WellKnownTypes' _SYNTAX._serialized_start=1413 _SYNTAX._serialized_end=1459 _TYPE._serialized_start=113 _TYPE._serialized_end=328 _FIELD._serialized_start=331 _FIELD._serialized_end=1056 _FIELD_KIND._serialized_start=610 _FIELD_KIND._serialized_end=938 _FIELD_CARDINALITY._serialized_start=940 _FIELD_CARDINALITY._serialized_end=1056 _ENUM._serialized_start=1059 _ENUM._serialized_end=1265 _ENUMVALUE._serialized_start=1267 _ENUMVALUE._serialized_end=1350 _OPTION._serialized_start=1352 _OPTION._serialized_end=1411 # @@protoc_insertion_point(module_scope) ================================================ FILE: openpype/hosts/hiero/vendor/google/protobuf/util/__init__.py ================================================ ================================================ FILE: openpype/hosts/hiero/vendor/google/protobuf/util/json_format_pb2.py ================================================ # -*- coding: utf-8 -*- # Generated by the protocol buffer compiler. DO NOT EDIT! # source: google/protobuf/util/json_format.proto """Generated protocol buffer code.""" from google.protobuf.internal import builder as _builder from google.protobuf import descriptor as _descriptor from google.protobuf import descriptor_pool as _descriptor_pool from google.protobuf import symbol_database as _symbol_database # @@protoc_insertion_point(imports) _sym_db = _symbol_database.Default() DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n&google/protobuf/util/json_format.proto\x12\x11protobuf_unittest\"\x89\x01\n\x13TestFlagsAndStrings\x12\t\n\x01\x41\x18\x01 \x02(\x05\x12K\n\rrepeatedgroup\x18\x02 \x03(\n24.protobuf_unittest.TestFlagsAndStrings.RepeatedGroup\x1a\x1a\n\rRepeatedGroup\x12\t\n\x01\x66\x18\x03 \x02(\t\"!\n\x14TestBase64ByteArrays\x12\t\n\x01\x61\x18\x01 \x02(\x0c\"G\n\x12TestJavaScriptJSON\x12\t\n\x01\x61\x18\x01 \x01(\x05\x12\r\n\x05\x66inal\x18\x02 \x01(\x02\x12\n\n\x02in\x18\x03 \x01(\t\x12\x0b\n\x03Var\x18\x04 \x01(\t\"Q\n\x18TestJavaScriptOrderJSON1\x12\t\n\x01\x64\x18\x01 \x01(\x05\x12\t\n\x01\x63\x18\x02 \x01(\x05\x12\t\n\x01x\x18\x03 \x01(\x08\x12\t\n\x01\x62\x18\x04 \x01(\x05\x12\t\n\x01\x61\x18\x05 \x01(\x05\"\x89\x01\n\x18TestJavaScriptOrderJSON2\x12\t\n\x01\x64\x18\x01 \x01(\x05\x12\t\n\x01\x63\x18\x02 \x01(\x05\x12\t\n\x01x\x18\x03 \x01(\x08\x12\t\n\x01\x62\x18\x04 \x01(\x05\x12\t\n\x01\x61\x18\x05 \x01(\x05\x12\x36\n\x01z\x18\x06 \x03(\x0b\x32+.protobuf_unittest.TestJavaScriptOrderJSON1\"$\n\x0cTestLargeInt\x12\t\n\x01\x61\x18\x01 \x02(\x03\x12\t\n\x01\x62\x18\x02 \x02(\x04\"\xa0\x01\n\x0bTestNumbers\x12\x30\n\x01\x61\x18\x01 \x01(\x0e\x32%.protobuf_unittest.TestNumbers.MyType\x12\t\n\x01\x62\x18\x02 \x01(\x05\x12\t\n\x01\x63\x18\x03 \x01(\x02\x12\t\n\x01\x64\x18\x04 \x01(\x08\x12\t\n\x01\x65\x18\x05 \x01(\x01\x12\t\n\x01\x66\x18\x06 \x01(\r\"(\n\x06MyType\x12\x06\n\x02OK\x10\x00\x12\x0b\n\x07WARNING\x10\x01\x12\t\n\x05\x45RROR\x10\x02\"T\n\rTestCamelCase\x12\x14\n\x0cnormal_field\x18\x01 \x01(\t\x12\x15\n\rCAPITAL_FIELD\x18\x02 \x01(\x05\x12\x16\n\x0e\x43\x61melCaseField\x18\x03 \x01(\x05\"|\n\x0bTestBoolMap\x12=\n\x08\x62ool_map\x18\x01 \x03(\x0b\x32+.protobuf_unittest.TestBoolMap.BoolMapEntry\x1a.\n\x0c\x42oolMapEntry\x12\x0b\n\x03key\x18\x01 \x01(\x08\x12\r\n\x05value\x18\x02 \x01(\x05:\x02\x38\x01\"O\n\rTestRecursion\x12\r\n\x05value\x18\x01 \x01(\x05\x12/\n\x05\x63hild\x18\x02 \x01(\x0b\x32 .protobuf_unittest.TestRecursion\"\x86\x01\n\rTestStringMap\x12\x43\n\nstring_map\x18\x01 \x03(\x0b\x32/.protobuf_unittest.TestStringMap.StringMapEntry\x1a\x30\n\x0eStringMapEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"\xc4\x01\n\x14TestStringSerializer\x12\x15\n\rscalar_string\x18\x01 \x01(\t\x12\x17\n\x0frepeated_string\x18\x02 \x03(\t\x12J\n\nstring_map\x18\x03 \x03(\x0b\x32\x36.protobuf_unittest.TestStringSerializer.StringMapEntry\x1a\x30\n\x0eStringMapEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"$\n\x18TestMessageWithExtension*\x08\x08\x64\x10\x80\x80\x80\x80\x02\"z\n\rTestExtension\x12\r\n\x05value\x18\x01 \x01(\t2Z\n\x03\x65xt\x12+.protobuf_unittest.TestMessageWithExtension\x18\x64 \x01(\x0b\x32 .protobuf_unittest.TestExtension\"Q\n\x14TestDefaultEnumValue\x12\x39\n\nenum_value\x18\x01 \x01(\x0e\x32\x1c.protobuf_unittest.EnumValue:\x07\x44\x45\x46\x41ULT*2\n\tEnumValue\x12\x0c\n\x08PROTOCOL\x10\x00\x12\n\n\x06\x42UFFER\x10\x01\x12\x0b\n\x07\x44\x45\x46\x41ULT\x10\x02') _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals()) _builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'google.protobuf.util.json_format_pb2', globals()) if _descriptor._USE_C_DESCRIPTORS == False: TestMessageWithExtension.RegisterExtension(_TESTEXTENSION.extensions_by_name['ext']) DESCRIPTOR._options = None _TESTBOOLMAP_BOOLMAPENTRY._options = None _TESTBOOLMAP_BOOLMAPENTRY._serialized_options = b'8\001' _TESTSTRINGMAP_STRINGMAPENTRY._options = None _TESTSTRINGMAP_STRINGMAPENTRY._serialized_options = b'8\001' _TESTSTRINGSERIALIZER_STRINGMAPENTRY._options = None _TESTSTRINGSERIALIZER_STRINGMAPENTRY._serialized_options = b'8\001' _ENUMVALUE._serialized_start=1607 _ENUMVALUE._serialized_end=1657 _TESTFLAGSANDSTRINGS._serialized_start=62 _TESTFLAGSANDSTRINGS._serialized_end=199 _TESTFLAGSANDSTRINGS_REPEATEDGROUP._serialized_start=173 _TESTFLAGSANDSTRINGS_REPEATEDGROUP._serialized_end=199 _TESTBASE64BYTEARRAYS._serialized_start=201 _TESTBASE64BYTEARRAYS._serialized_end=234 _TESTJAVASCRIPTJSON._serialized_start=236 _TESTJAVASCRIPTJSON._serialized_end=307 _TESTJAVASCRIPTORDERJSON1._serialized_start=309 _TESTJAVASCRIPTORDERJSON1._serialized_end=390 _TESTJAVASCRIPTORDERJSON2._serialized_start=393 _TESTJAVASCRIPTORDERJSON2._serialized_end=530 _TESTLARGEINT._serialized_start=532 _TESTLARGEINT._serialized_end=568 _TESTNUMBERS._serialized_start=571 _TESTNUMBERS._serialized_end=731 _TESTNUMBERS_MYTYPE._serialized_start=691 _TESTNUMBERS_MYTYPE._serialized_end=731 _TESTCAMELCASE._serialized_start=733 _TESTCAMELCASE._serialized_end=817 _TESTBOOLMAP._serialized_start=819 _TESTBOOLMAP._serialized_end=943 _TESTBOOLMAP_BOOLMAPENTRY._serialized_start=897 _TESTBOOLMAP_BOOLMAPENTRY._serialized_end=943 _TESTRECURSION._serialized_start=945 _TESTRECURSION._serialized_end=1024 _TESTSTRINGMAP._serialized_start=1027 _TESTSTRINGMAP._serialized_end=1161 _TESTSTRINGMAP_STRINGMAPENTRY._serialized_start=1113 _TESTSTRINGMAP_STRINGMAPENTRY._serialized_end=1161 _TESTSTRINGSERIALIZER._serialized_start=1164 _TESTSTRINGSERIALIZER._serialized_end=1360 _TESTSTRINGSERIALIZER_STRINGMAPENTRY._serialized_start=1113 _TESTSTRINGSERIALIZER_STRINGMAPENTRY._serialized_end=1161 _TESTMESSAGEWITHEXTENSION._serialized_start=1362 _TESTMESSAGEWITHEXTENSION._serialized_end=1398 _TESTEXTENSION._serialized_start=1400 _TESTEXTENSION._serialized_end=1522 _TESTDEFAULTENUMVALUE._serialized_start=1524 _TESTDEFAULTENUMVALUE._serialized_end=1605 # @@protoc_insertion_point(module_scope) ================================================ FILE: openpype/hosts/hiero/vendor/google/protobuf/util/json_format_proto3_pb2.py ================================================ # -*- coding: utf-8 -*- # Generated by the protocol buffer compiler. DO NOT EDIT! # source: google/protobuf/util/json_format_proto3.proto """Generated protocol buffer code.""" from google.protobuf.internal import builder as _builder from google.protobuf import descriptor as _descriptor from google.protobuf import descriptor_pool as _descriptor_pool from google.protobuf import symbol_database as _symbol_database # @@protoc_insertion_point(imports) _sym_db = _symbol_database.Default() from google.protobuf import any_pb2 as google_dot_protobuf_dot_any__pb2 from google.protobuf import duration_pb2 as google_dot_protobuf_dot_duration__pb2 from google.protobuf import field_mask_pb2 as google_dot_protobuf_dot_field__mask__pb2 from google.protobuf import struct_pb2 as google_dot_protobuf_dot_struct__pb2 from google.protobuf import timestamp_pb2 as google_dot_protobuf_dot_timestamp__pb2 from google.protobuf import wrappers_pb2 as google_dot_protobuf_dot_wrappers__pb2 from google.protobuf import unittest_pb2 as google_dot_protobuf_dot_unittest__pb2 DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n-google/protobuf/util/json_format_proto3.proto\x12\x06proto3\x1a\x19google/protobuf/any.proto\x1a\x1egoogle/protobuf/duration.proto\x1a google/protobuf/field_mask.proto\x1a\x1cgoogle/protobuf/struct.proto\x1a\x1fgoogle/protobuf/timestamp.proto\x1a\x1egoogle/protobuf/wrappers.proto\x1a\x1egoogle/protobuf/unittest.proto\"\x1c\n\x0bMessageType\x12\r\n\x05value\x18\x01 \x01(\x05\"\x94\x05\n\x0bTestMessage\x12\x12\n\nbool_value\x18\x01 \x01(\x08\x12\x13\n\x0bint32_value\x18\x02 \x01(\x05\x12\x13\n\x0bint64_value\x18\x03 \x01(\x03\x12\x14\n\x0cuint32_value\x18\x04 \x01(\r\x12\x14\n\x0cuint64_value\x18\x05 \x01(\x04\x12\x13\n\x0b\x66loat_value\x18\x06 \x01(\x02\x12\x14\n\x0c\x64ouble_value\x18\x07 \x01(\x01\x12\x14\n\x0cstring_value\x18\x08 \x01(\t\x12\x13\n\x0b\x62ytes_value\x18\t \x01(\x0c\x12$\n\nenum_value\x18\n \x01(\x0e\x32\x10.proto3.EnumType\x12*\n\rmessage_value\x18\x0b \x01(\x0b\x32\x13.proto3.MessageType\x12\x1b\n\x13repeated_bool_value\x18\x15 \x03(\x08\x12\x1c\n\x14repeated_int32_value\x18\x16 \x03(\x05\x12\x1c\n\x14repeated_int64_value\x18\x17 \x03(\x03\x12\x1d\n\x15repeated_uint32_value\x18\x18 \x03(\r\x12\x1d\n\x15repeated_uint64_value\x18\x19 \x03(\x04\x12\x1c\n\x14repeated_float_value\x18\x1a \x03(\x02\x12\x1d\n\x15repeated_double_value\x18\x1b \x03(\x01\x12\x1d\n\x15repeated_string_value\x18\x1c \x03(\t\x12\x1c\n\x14repeated_bytes_value\x18\x1d \x03(\x0c\x12-\n\x13repeated_enum_value\x18\x1e \x03(\x0e\x32\x10.proto3.EnumType\x12\x33\n\x16repeated_message_value\x18\x1f \x03(\x0b\x32\x13.proto3.MessageType\"\x8c\x02\n\tTestOneof\x12\x1b\n\x11oneof_int32_value\x18\x01 \x01(\x05H\x00\x12\x1c\n\x12oneof_string_value\x18\x02 \x01(\tH\x00\x12\x1b\n\x11oneof_bytes_value\x18\x03 \x01(\x0cH\x00\x12,\n\x10oneof_enum_value\x18\x04 \x01(\x0e\x32\x10.proto3.EnumTypeH\x00\x12\x32\n\x13oneof_message_value\x18\x05 \x01(\x0b\x32\x13.proto3.MessageTypeH\x00\x12\x36\n\x10oneof_null_value\x18\x06 \x01(\x0e\x32\x1a.google.protobuf.NullValueH\x00\x42\r\n\x0boneof_value\"\xe1\x04\n\x07TestMap\x12.\n\x08\x62ool_map\x18\x01 \x03(\x0b\x32\x1c.proto3.TestMap.BoolMapEntry\x12\x30\n\tint32_map\x18\x02 \x03(\x0b\x32\x1d.proto3.TestMap.Int32MapEntry\x12\x30\n\tint64_map\x18\x03 \x03(\x0b\x32\x1d.proto3.TestMap.Int64MapEntry\x12\x32\n\nuint32_map\x18\x04 \x03(\x0b\x32\x1e.proto3.TestMap.Uint32MapEntry\x12\x32\n\nuint64_map\x18\x05 \x03(\x0b\x32\x1e.proto3.TestMap.Uint64MapEntry\x12\x32\n\nstring_map\x18\x06 \x03(\x0b\x32\x1e.proto3.TestMap.StringMapEntry\x1a.\n\x0c\x42oolMapEntry\x12\x0b\n\x03key\x18\x01 \x01(\x08\x12\r\n\x05value\x18\x02 \x01(\x05:\x02\x38\x01\x1a/\n\rInt32MapEntry\x12\x0b\n\x03key\x18\x01 \x01(\x05\x12\r\n\x05value\x18\x02 \x01(\x05:\x02\x38\x01\x1a/\n\rInt64MapEntry\x12\x0b\n\x03key\x18\x01 \x01(\x03\x12\r\n\x05value\x18\x02 \x01(\x05:\x02\x38\x01\x1a\x30\n\x0eUint32MapEntry\x12\x0b\n\x03key\x18\x01 \x01(\r\x12\r\n\x05value\x18\x02 \x01(\x05:\x02\x38\x01\x1a\x30\n\x0eUint64MapEntry\x12\x0b\n\x03key\x18\x01 \x01(\x04\x12\r\n\x05value\x18\x02 \x01(\x05:\x02\x38\x01\x1a\x30\n\x0eStringMapEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\x05:\x02\x38\x01\"\x85\x06\n\rTestNestedMap\x12\x34\n\x08\x62ool_map\x18\x01 \x03(\x0b\x32\".proto3.TestNestedMap.BoolMapEntry\x12\x36\n\tint32_map\x18\x02 \x03(\x0b\x32#.proto3.TestNestedMap.Int32MapEntry\x12\x36\n\tint64_map\x18\x03 \x03(\x0b\x32#.proto3.TestNestedMap.Int64MapEntry\x12\x38\n\nuint32_map\x18\x04 \x03(\x0b\x32$.proto3.TestNestedMap.Uint32MapEntry\x12\x38\n\nuint64_map\x18\x05 \x03(\x0b\x32$.proto3.TestNestedMap.Uint64MapEntry\x12\x38\n\nstring_map\x18\x06 \x03(\x0b\x32$.proto3.TestNestedMap.StringMapEntry\x12\x32\n\x07map_map\x18\x07 \x03(\x0b\x32!.proto3.TestNestedMap.MapMapEntry\x1a.\n\x0c\x42oolMapEntry\x12\x0b\n\x03key\x18\x01 \x01(\x08\x12\r\n\x05value\x18\x02 \x01(\x05:\x02\x38\x01\x1a/\n\rInt32MapEntry\x12\x0b\n\x03key\x18\x01 \x01(\x05\x12\r\n\x05value\x18\x02 \x01(\x05:\x02\x38\x01\x1a/\n\rInt64MapEntry\x12\x0b\n\x03key\x18\x01 \x01(\x03\x12\r\n\x05value\x18\x02 \x01(\x05:\x02\x38\x01\x1a\x30\n\x0eUint32MapEntry\x12\x0b\n\x03key\x18\x01 \x01(\r\x12\r\n\x05value\x18\x02 \x01(\x05:\x02\x38\x01\x1a\x30\n\x0eUint64MapEntry\x12\x0b\n\x03key\x18\x01 \x01(\x04\x12\r\n\x05value\x18\x02 \x01(\x05:\x02\x38\x01\x1a\x30\n\x0eStringMapEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\x05:\x02\x38\x01\x1a\x44\n\x0bMapMapEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12$\n\x05value\x18\x02 \x01(\x0b\x32\x15.proto3.TestNestedMap:\x02\x38\x01\"{\n\rTestStringMap\x12\x38\n\nstring_map\x18\x01 \x03(\x0b\x32$.proto3.TestStringMap.StringMapEntry\x1a\x30\n\x0eStringMapEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"\xee\x07\n\x0bTestWrapper\x12.\n\nbool_value\x18\x01 \x01(\x0b\x32\x1a.google.protobuf.BoolValue\x12\x30\n\x0bint32_value\x18\x02 \x01(\x0b\x32\x1b.google.protobuf.Int32Value\x12\x30\n\x0bint64_value\x18\x03 \x01(\x0b\x32\x1b.google.protobuf.Int64Value\x12\x32\n\x0cuint32_value\x18\x04 \x01(\x0b\x32\x1c.google.protobuf.UInt32Value\x12\x32\n\x0cuint64_value\x18\x05 \x01(\x0b\x32\x1c.google.protobuf.UInt64Value\x12\x30\n\x0b\x66loat_value\x18\x06 \x01(\x0b\x32\x1b.google.protobuf.FloatValue\x12\x32\n\x0c\x64ouble_value\x18\x07 \x01(\x0b\x32\x1c.google.protobuf.DoubleValue\x12\x32\n\x0cstring_value\x18\x08 \x01(\x0b\x32\x1c.google.protobuf.StringValue\x12\x30\n\x0b\x62ytes_value\x18\t \x01(\x0b\x32\x1b.google.protobuf.BytesValue\x12\x37\n\x13repeated_bool_value\x18\x0b \x03(\x0b\x32\x1a.google.protobuf.BoolValue\x12\x39\n\x14repeated_int32_value\x18\x0c \x03(\x0b\x32\x1b.google.protobuf.Int32Value\x12\x39\n\x14repeated_int64_value\x18\r \x03(\x0b\x32\x1b.google.protobuf.Int64Value\x12;\n\x15repeated_uint32_value\x18\x0e \x03(\x0b\x32\x1c.google.protobuf.UInt32Value\x12;\n\x15repeated_uint64_value\x18\x0f \x03(\x0b\x32\x1c.google.protobuf.UInt64Value\x12\x39\n\x14repeated_float_value\x18\x10 \x03(\x0b\x32\x1b.google.protobuf.FloatValue\x12;\n\x15repeated_double_value\x18\x11 \x03(\x0b\x32\x1c.google.protobuf.DoubleValue\x12;\n\x15repeated_string_value\x18\x12 \x03(\x0b\x32\x1c.google.protobuf.StringValue\x12\x39\n\x14repeated_bytes_value\x18\x13 \x03(\x0b\x32\x1b.google.protobuf.BytesValue\"n\n\rTestTimestamp\x12)\n\x05value\x18\x01 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x32\n\x0erepeated_value\x18\x02 \x03(\x0b\x32\x1a.google.protobuf.Timestamp\"k\n\x0cTestDuration\x12(\n\x05value\x18\x01 \x01(\x0b\x32\x19.google.protobuf.Duration\x12\x31\n\x0erepeated_value\x18\x02 \x03(\x0b\x32\x19.google.protobuf.Duration\":\n\rTestFieldMask\x12)\n\x05value\x18\x01 \x01(\x0b\x32\x1a.google.protobuf.FieldMask\"e\n\nTestStruct\x12&\n\x05value\x18\x01 \x01(\x0b\x32\x17.google.protobuf.Struct\x12/\n\x0erepeated_value\x18\x02 \x03(\x0b\x32\x17.google.protobuf.Struct\"\\\n\x07TestAny\x12#\n\x05value\x18\x01 \x01(\x0b\x32\x14.google.protobuf.Any\x12,\n\x0erepeated_value\x18\x02 \x03(\x0b\x32\x14.google.protobuf.Any\"b\n\tTestValue\x12%\n\x05value\x18\x01 \x01(\x0b\x32\x16.google.protobuf.Value\x12.\n\x0erepeated_value\x18\x02 \x03(\x0b\x32\x16.google.protobuf.Value\"n\n\rTestListValue\x12)\n\x05value\x18\x01 \x01(\x0b\x32\x1a.google.protobuf.ListValue\x12\x32\n\x0erepeated_value\x18\x02 \x03(\x0b\x32\x1a.google.protobuf.ListValue\"\x89\x01\n\rTestBoolValue\x12\x12\n\nbool_value\x18\x01 \x01(\x08\x12\x34\n\x08\x62ool_map\x18\x02 \x03(\x0b\x32\".proto3.TestBoolValue.BoolMapEntry\x1a.\n\x0c\x42oolMapEntry\x12\x0b\n\x03key\x18\x01 \x01(\x08\x12\r\n\x05value\x18\x02 \x01(\x05:\x02\x38\x01\"+\n\x12TestCustomJsonName\x12\x15\n\x05value\x18\x01 \x01(\x05R\x06@value\"J\n\x0eTestExtensions\x12\x38\n\nextensions\x18\x01 \x01(\x0b\x32$.protobuf_unittest.TestAllExtensions\"\x84\x01\n\rTestEnumValue\x12%\n\x0b\x65num_value1\x18\x01 \x01(\x0e\x32\x10.proto3.EnumType\x12%\n\x0b\x65num_value2\x18\x02 \x01(\x0e\x32\x10.proto3.EnumType\x12%\n\x0b\x65num_value3\x18\x03 \x01(\x0e\x32\x10.proto3.EnumType*\x1c\n\x08\x45numType\x12\x07\n\x03\x46OO\x10\x00\x12\x07\n\x03\x42\x41R\x10\x01\x42,\n\x18\x63om.google.protobuf.utilB\x10JsonFormatProto3b\x06proto3') _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals()) _builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'google.protobuf.util.json_format_proto3_pb2', globals()) if _descriptor._USE_C_DESCRIPTORS == False: DESCRIPTOR._options = None DESCRIPTOR._serialized_options = b'\n\030com.google.protobuf.utilB\020JsonFormatProto3' _TESTMAP_BOOLMAPENTRY._options = None _TESTMAP_BOOLMAPENTRY._serialized_options = b'8\001' _TESTMAP_INT32MAPENTRY._options = None _TESTMAP_INT32MAPENTRY._serialized_options = b'8\001' _TESTMAP_INT64MAPENTRY._options = None _TESTMAP_INT64MAPENTRY._serialized_options = b'8\001' _TESTMAP_UINT32MAPENTRY._options = None _TESTMAP_UINT32MAPENTRY._serialized_options = b'8\001' _TESTMAP_UINT64MAPENTRY._options = None _TESTMAP_UINT64MAPENTRY._serialized_options = b'8\001' _TESTMAP_STRINGMAPENTRY._options = None _TESTMAP_STRINGMAPENTRY._serialized_options = b'8\001' _TESTNESTEDMAP_BOOLMAPENTRY._options = None _TESTNESTEDMAP_BOOLMAPENTRY._serialized_options = b'8\001' _TESTNESTEDMAP_INT32MAPENTRY._options = None _TESTNESTEDMAP_INT32MAPENTRY._serialized_options = b'8\001' _TESTNESTEDMAP_INT64MAPENTRY._options = None _TESTNESTEDMAP_INT64MAPENTRY._serialized_options = b'8\001' _TESTNESTEDMAP_UINT32MAPENTRY._options = None _TESTNESTEDMAP_UINT32MAPENTRY._serialized_options = b'8\001' _TESTNESTEDMAP_UINT64MAPENTRY._options = None _TESTNESTEDMAP_UINT64MAPENTRY._serialized_options = b'8\001' _TESTNESTEDMAP_STRINGMAPENTRY._options = None _TESTNESTEDMAP_STRINGMAPENTRY._serialized_options = b'8\001' _TESTNESTEDMAP_MAPMAPENTRY._options = None _TESTNESTEDMAP_MAPMAPENTRY._serialized_options = b'8\001' _TESTSTRINGMAP_STRINGMAPENTRY._options = None _TESTSTRINGMAP_STRINGMAPENTRY._serialized_options = b'8\001' _TESTBOOLVALUE_BOOLMAPENTRY._options = None _TESTBOOLVALUE_BOOLMAPENTRY._serialized_options = b'8\001' _ENUMTYPE._serialized_start=4849 _ENUMTYPE._serialized_end=4877 _MESSAGETYPE._serialized_start=277 _MESSAGETYPE._serialized_end=305 _TESTMESSAGE._serialized_start=308 _TESTMESSAGE._serialized_end=968 _TESTONEOF._serialized_start=971 _TESTONEOF._serialized_end=1239 _TESTMAP._serialized_start=1242 _TESTMAP._serialized_end=1851 _TESTMAP_BOOLMAPENTRY._serialized_start=1557 _TESTMAP_BOOLMAPENTRY._serialized_end=1603 _TESTMAP_INT32MAPENTRY._serialized_start=1605 _TESTMAP_INT32MAPENTRY._serialized_end=1652 _TESTMAP_INT64MAPENTRY._serialized_start=1654 _TESTMAP_INT64MAPENTRY._serialized_end=1701 _TESTMAP_UINT32MAPENTRY._serialized_start=1703 _TESTMAP_UINT32MAPENTRY._serialized_end=1751 _TESTMAP_UINT64MAPENTRY._serialized_start=1753 _TESTMAP_UINT64MAPENTRY._serialized_end=1801 _TESTMAP_STRINGMAPENTRY._serialized_start=1803 _TESTMAP_STRINGMAPENTRY._serialized_end=1851 _TESTNESTEDMAP._serialized_start=1854 _TESTNESTEDMAP._serialized_end=2627 _TESTNESTEDMAP_BOOLMAPENTRY._serialized_start=1557 _TESTNESTEDMAP_BOOLMAPENTRY._serialized_end=1603 _TESTNESTEDMAP_INT32MAPENTRY._serialized_start=1605 _TESTNESTEDMAP_INT32MAPENTRY._serialized_end=1652 _TESTNESTEDMAP_INT64MAPENTRY._serialized_start=1654 _TESTNESTEDMAP_INT64MAPENTRY._serialized_end=1701 _TESTNESTEDMAP_UINT32MAPENTRY._serialized_start=1703 _TESTNESTEDMAP_UINT32MAPENTRY._serialized_end=1751 _TESTNESTEDMAP_UINT64MAPENTRY._serialized_start=1753 _TESTNESTEDMAP_UINT64MAPENTRY._serialized_end=1801 _TESTNESTEDMAP_STRINGMAPENTRY._serialized_start=1803 _TESTNESTEDMAP_STRINGMAPENTRY._serialized_end=1851 _TESTNESTEDMAP_MAPMAPENTRY._serialized_start=2559 _TESTNESTEDMAP_MAPMAPENTRY._serialized_end=2627 _TESTSTRINGMAP._serialized_start=2629 _TESTSTRINGMAP._serialized_end=2752 _TESTSTRINGMAP_STRINGMAPENTRY._serialized_start=2704 _TESTSTRINGMAP_STRINGMAPENTRY._serialized_end=2752 _TESTWRAPPER._serialized_start=2755 _TESTWRAPPER._serialized_end=3761 _TESTTIMESTAMP._serialized_start=3763 _TESTTIMESTAMP._serialized_end=3873 _TESTDURATION._serialized_start=3875 _TESTDURATION._serialized_end=3982 _TESTFIELDMASK._serialized_start=3984 _TESTFIELDMASK._serialized_end=4042 _TESTSTRUCT._serialized_start=4044 _TESTSTRUCT._serialized_end=4145 _TESTANY._serialized_start=4147 _TESTANY._serialized_end=4239 _TESTVALUE._serialized_start=4241 _TESTVALUE._serialized_end=4339 _TESTLISTVALUE._serialized_start=4341 _TESTLISTVALUE._serialized_end=4451 _TESTBOOLVALUE._serialized_start=4454 _TESTBOOLVALUE._serialized_end=4591 _TESTBOOLVALUE_BOOLMAPENTRY._serialized_start=1557 _TESTBOOLVALUE_BOOLMAPENTRY._serialized_end=1603 _TESTCUSTOMJSONNAME._serialized_start=4593 _TESTCUSTOMJSONNAME._serialized_end=4636 _TESTEXTENSIONS._serialized_start=4638 _TESTEXTENSIONS._serialized_end=4712 _TESTENUMVALUE._serialized_start=4715 _TESTENUMVALUE._serialized_end=4847 # @@protoc_insertion_point(module_scope) ================================================ FILE: openpype/hosts/hiero/vendor/google/protobuf/wrappers_pb2.py ================================================ # -*- coding: utf-8 -*- # Generated by the protocol buffer compiler. DO NOT EDIT! # source: google/protobuf/wrappers.proto """Generated protocol buffer code.""" from google.protobuf.internal import builder as _builder from google.protobuf import descriptor as _descriptor from google.protobuf import descriptor_pool as _descriptor_pool from google.protobuf import symbol_database as _symbol_database # @@protoc_insertion_point(imports) _sym_db = _symbol_database.Default() DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x1egoogle/protobuf/wrappers.proto\x12\x0fgoogle.protobuf\"\x1c\n\x0b\x44oubleValue\x12\r\n\x05value\x18\x01 \x01(\x01\"\x1b\n\nFloatValue\x12\r\n\x05value\x18\x01 \x01(\x02\"\x1b\n\nInt64Value\x12\r\n\x05value\x18\x01 \x01(\x03\"\x1c\n\x0bUInt64Value\x12\r\n\x05value\x18\x01 \x01(\x04\"\x1b\n\nInt32Value\x12\r\n\x05value\x18\x01 \x01(\x05\"\x1c\n\x0bUInt32Value\x12\r\n\x05value\x18\x01 \x01(\r\"\x1a\n\tBoolValue\x12\r\n\x05value\x18\x01 \x01(\x08\"\x1c\n\x0bStringValue\x12\r\n\x05value\x18\x01 \x01(\t\"\x1b\n\nBytesValue\x12\r\n\x05value\x18\x01 \x01(\x0c\x42\x83\x01\n\x13\x63om.google.protobufB\rWrappersProtoP\x01Z1google.golang.org/protobuf/types/known/wrapperspb\xf8\x01\x01\xa2\x02\x03GPB\xaa\x02\x1eGoogle.Protobuf.WellKnownTypesb\x06proto3') _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals()) _builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'google.protobuf.wrappers_pb2', globals()) if _descriptor._USE_C_DESCRIPTORS == False: DESCRIPTOR._options = None DESCRIPTOR._serialized_options = b'\n\023com.google.protobufB\rWrappersProtoP\001Z1google.golang.org/protobuf/types/known/wrapperspb\370\001\001\242\002\003GPB\252\002\036Google.Protobuf.WellKnownTypes' _DOUBLEVALUE._serialized_start=51 _DOUBLEVALUE._serialized_end=79 _FLOATVALUE._serialized_start=81 _FLOATVALUE._serialized_end=108 _INT64VALUE._serialized_start=110 _INT64VALUE._serialized_end=137 _UINT64VALUE._serialized_start=139 _UINT64VALUE._serialized_end=167 _INT32VALUE._serialized_start=169 _INT32VALUE._serialized_end=196 _UINT32VALUE._serialized_start=198 _UINT32VALUE._serialized_end=226 _BOOLVALUE._serialized_start=228 _BOOLVALUE._serialized_end=254 _STRINGVALUE._serialized_start=256 _STRINGVALUE._serialized_end=284 _BYTESVALUE._serialized_start=286 _BYTESVALUE._serialized_end=313 # @@protoc_insertion_point(module_scope) ================================================ FILE: openpype/hosts/houdini/__init__.py ================================================ from .addon import ( HoudiniAddon, HOUDINI_HOST_DIR, ) __all__ = ( "HoudiniAddon", "HOUDINI_HOST_DIR", ) ================================================ FILE: openpype/hosts/houdini/addon.py ================================================ import os from openpype.modules import OpenPypeModule, IHostAddon HOUDINI_HOST_DIR = os.path.dirname(os.path.abspath(__file__)) class HoudiniAddon(OpenPypeModule, IHostAddon): name = "houdini" host_name = "houdini" def initialize(self, module_settings): self.enabled = True def add_implementation_envs(self, env, _app): # Add requirements to HOUDINI_PATH and HOUDINI_MENU_PATH startup_path = os.path.join(HOUDINI_HOST_DIR, "startup") new_houdini_path = [startup_path] new_houdini_menu_path = [startup_path] old_houdini_path = env.get("HOUDINI_PATH") or "" old_houdini_menu_path = env.get("HOUDINI_MENU_PATH") or "" for path in old_houdini_path.split(os.pathsep): if not path: continue norm_path = os.path.normpath(path) if norm_path not in new_houdini_path: new_houdini_path.append(norm_path) for path in old_houdini_menu_path.split(os.pathsep): if not path: continue norm_path = os.path.normpath(path) if norm_path not in new_houdini_menu_path: new_houdini_menu_path.append(norm_path) # Add ampersand for unknown reason (Maybe is needed in Houdini?) new_houdini_path.append("&") new_houdini_menu_path.append("&") env["HOUDINI_PATH"] = os.pathsep.join(new_houdini_path) env["HOUDINI_MENU_PATH"] = os.pathsep.join(new_houdini_menu_path) def get_launch_hook_paths(self, app): if app.host_name != self.host_name: return [] return [ os.path.join(HOUDINI_HOST_DIR, "hooks") ] def get_workfile_extensions(self): return [".hip", ".hiplc", ".hipnc"] ================================================ FILE: openpype/hosts/houdini/api/__init__.py ================================================ from .pipeline import ( HoudiniHost, ls, containerise ) from .plugin import ( Creator, ) from .lib import ( lsattr, lsattrs, read, maintained_selection ) __all__ = [ "HoudiniHost", "ls", "containerise", "Creator", # Utility functions "lsattr", "lsattrs", "read", "maintained_selection" ] ================================================ FILE: openpype/hosts/houdini/api/action.py ================================================ import pyblish.api import hou from openpype.pipeline.publish import get_errored_instances_from_context class SelectInvalidAction(pyblish.api.Action): """Select invalid nodes in Maya when plug-in failed. To retrieve the invalid nodes this assumes a static `get_invalid()` method is available on the plugin. """ label = "Select invalid" on = "failed" # This action is only available on a failed plug-in icon = "search" # Icon from Awesome Icon def process(self, context, plugin): errored_instances = get_errored_instances_from_context(context, plugin=plugin) # Get the invalid nodes for the plug-ins self.log.info("Finding invalid nodes..") invalid = list() for instance in errored_instances: invalid_nodes = plugin.get_invalid(instance) if invalid_nodes: if isinstance(invalid_nodes, (list, tuple)): invalid.extend(invalid_nodes) else: self.log.warning("Plug-in returned to be invalid, " "but has no selectable nodes.") hou.clearAllSelected() if invalid: self.log.info("Selecting invalid nodes: {}".format( ", ".join(node.path() for node in invalid) )) for node in invalid: node.setSelected(True) node.setCurrent(True) else: self.log.info("No invalid nodes found.") class SelectROPAction(pyblish.api.Action): """Select ROP. It's used to select the associated ROPs with the errored instances. """ label = "Select ROP" on = "failed" # This action is only available on a failed plug-in icon = "mdi.cursor-default-click" def process(self, context, plugin): errored_instances = get_errored_instances_from_context(context, plugin) # Get the invalid nodes for the plug-ins self.log.info("Finding ROP nodes..") rop_nodes = list() for instance in errored_instances: node_path = instance.data.get("instance_node") if not node_path: continue node = hou.node(node_path) if not node: continue rop_nodes.append(node) hou.clearAllSelected() if rop_nodes: self.log.info("Selecting ROP nodes: {}".format( ", ".join(node.path() for node in rop_nodes) )) for node in rop_nodes: node.setSelected(True) node.setCurrent(True) else: self.log.info("No ROP nodes found.") ================================================ FILE: openpype/hosts/houdini/api/colorspace.py ================================================ import attr import hou from openpype.hosts.houdini.api.lib import get_color_management_preferences from openpype.pipeline.colorspace import get_display_view_colorspace_name @attr.s class LayerMetadata(object): """Data class for Render Layer metadata.""" frameStart = attr.ib() frameEnd = attr.ib() @attr.s class RenderProduct(object): """Getting Colorspace as Specific Render Product Parameter for submitting publish job. """ colorspace = attr.ib() # colorspace view = attr.ib() productName = attr.ib(default=None) class ARenderProduct(object): def __init__(self): """Constructor.""" # Initialize self.layer_data = self._get_layer_data() self.layer_data.products = self.get_colorspace_data() def _get_layer_data(self): return LayerMetadata( frameStart=int(hou.playbar.frameRange()[0]), frameEnd=int(hou.playbar.frameRange()[1]), ) def get_colorspace_data(self): """To be implemented by renderer class. This should return a list of RenderProducts. Returns: list: List of RenderProduct """ data = get_color_management_preferences() colorspace_data = [ RenderProduct( colorspace=data["display"], view=data["view"], productName="" ) ] return colorspace_data def get_default_display_view_colorspace(): """Returns the colorspace attribute of the default (display, view) pair. It's used for 'ociocolorspace' parm in OpenGL Node.""" prefs = get_color_management_preferences() return get_display_view_colorspace_name( config_path=prefs["config"], display=prefs["display"], view=prefs["view"] ) ================================================ FILE: openpype/hosts/houdini/api/creator_node_shelves.py ================================================ """Library to register OpenPype Creators for Houdini TAB node search menu. This can be used to install custom houdini tools for the TAB search menu which will trigger a publish instance to be created interactively. The Creators are automatically registered on launch of Houdini through the Houdini integration's `host.install()` method. """ import contextlib import tempfile import logging import os from openpype.client import get_asset_by_name from openpype.pipeline import registered_host from openpype.pipeline.create import CreateContext from openpype.resources import get_openpype_icon_filepath import hou import stateutils import soptoolutils import loptoolutils import cop2toolutils log = logging.getLogger(__name__) CATEGORY_GENERIC_TOOL = { hou.sopNodeTypeCategory(): soptoolutils.genericTool, hou.cop2NodeTypeCategory(): cop2toolutils.genericTool, hou.lopNodeTypeCategory(): loptoolutils.genericTool } CREATE_SCRIPT = """ from openpype.hosts.houdini.api.creator_node_shelves import create_interactive create_interactive("{identifier}", **kwargs) """ def create_interactive(creator_identifier, **kwargs): """Create a Creator using its identifier interactively. This is used by the generated shelf tools as callback when a user selects the creator from the node tab search menu. The `kwargs` should be what Houdini passes to the tool create scripts context. For more information see: https://www.sidefx.com/docs/houdini/hom/tool_script.html#arguments Args: creator_identifier (str): The creator identifier of the Creator plugin to create. Return: list: The created instances. """ host = registered_host() context = CreateContext(host) creator = context.manual_creators.get(creator_identifier) if not creator: raise RuntimeError("Invalid creator identifier: {}".format( creator_identifier) ) # TODO Use Qt instead result, variant = hou.ui.readInput( "Define variant name", buttons=("Ok", "Cancel"), initial_contents=creator.get_default_variant(), title="Define variant", help="Set the variant for the publish instance", close_choice=1 ) if result == 1: # User interrupted return variant = variant.strip() if not variant: raise RuntimeError("Empty variant value entered.") # TODO: Once more elaborate unique create behavior should exist per Creator # instead of per network editor area then we should move this from here # to a method on the Creators for which this could be the default # implementation. pane = stateutils.activePane(kwargs) if isinstance(pane, hou.NetworkEditor): pwd = pane.pwd() subset_name = creator.get_subset_name( variant=variant, task_name=context.get_current_task_name(), asset_doc=get_asset_by_name( project_name=context.get_current_project_name(), asset_name=context.get_current_asset_name() ), project_name=context.get_current_project_name(), host_name=context.host_name ) tool_fn = CATEGORY_GENERIC_TOOL.get(pwd.childTypeCategory()) if tool_fn is not None: out_null = tool_fn(kwargs, "null") out_null.setName("OUT_{}".format(subset_name), unique_name=True) before = context.instances_by_id.copy() # Create the instance context.create( creator_identifier=creator_identifier, variant=variant, pre_create_data={"use_selection": True} ) # For convenience we set the new node as current since that's much more # familiar to the artist when creating a node interactively # TODO Allow to disable auto-select in studio settings or user preferences after = context.instances_by_id new = set(after) - set(before) if new: # Select the new instance for instance_id in new: instance = after[instance_id] node = hou.node(instance.get("instance_node")) node.setCurrent(True) return list(new) @contextlib.contextmanager def shelves_change_block(): """Write shelf changes at the end of the context.""" hou.shelves.beginChangeBlock() try: yield finally: hou.shelves.endChangeBlock() def install(): """Install the Creator plug-ins to show in Houdini's TAB node search menu. This function is re-entrant and can be called again to reinstall and update the node definitions. For example during development it can be useful to call it manually: >>> from openpype.hosts.houdini.api.creator_node_shelves import install >>> install() Returns: list: List of `hou.Tool` instances """ host = registered_host() # Store the filepath on the host # TODO: Define a less hacky static shelf path for current houdini session filepath_attr = "_creator_node_shelf_filepath" filepath = getattr(host, filepath_attr, None) if filepath is None: f = tempfile.NamedTemporaryFile(prefix="houdini_creator_nodes_", suffix=".shelf", delete=False) f.close() filepath = f.name setattr(host, filepath_attr, filepath) elif os.path.exists(filepath): # Remove any existing shelf file so that we can completey regenerate # and update the tools file if creator identifiers change os.remove(filepath) icon = get_openpype_icon_filepath() tab_menu_label = os.environ.get("AVALON_LABEL") or "AYON" # Create context only to get creator plugins, so we don't reset and only # populate what we need to retrieve the list of creator plugins create_context = CreateContext(host, reset=False) create_context.reset_current_context() create_context._reset_creator_plugins() log.debug("Writing OpenPype Creator nodes to shelf: {}".format(filepath)) tools = [] with shelves_change_block(): for identifier, creator in create_context.manual_creators.items(): # Allow the creator plug-in itself to override the categories # for where they are shown with `Creator.get_network_categories()` if not hasattr(creator, "get_network_categories"): log.debug("Creator {} has no `get_network_categories` method " "and will not be added to TAB search.") continue network_categories = creator.get_network_categories() if not network_categories: continue key = "ayon_create.{}".format(identifier) log.debug(f"Registering {key}") script = CREATE_SCRIPT.format(identifier=identifier) data = { "script": script, "language": hou.scriptLanguage.Python, "icon": icon, "help": "Create Ayon publish instance for {}".format( creator.label ), "help_url": None, "network_categories": network_categories, "viewer_categories": [], "cop_viewer_categories": [], "network_op_type": None, "viewer_op_type": None, "locations": [tab_menu_label] } label = "Create {}".format(creator.label) tool = hou.shelves.tool(key) if tool: tool.setData(**data) tool.setLabel(label) else: tool = hou.shelves.newTool( file_path=filepath, name=key, label=label, **data ) tools.append(tool) # Ensure the shelf is reloaded hou.shelves.loadFile(filepath) return tools ================================================ FILE: openpype/hosts/houdini/api/lib.py ================================================ # -*- coding: utf-8 -*- import sys import os import errno import re import uuid import logging from contextlib import contextmanager import json import six from openpype.lib import StringTemplate from openpype.client import get_project, get_asset_by_name from openpype.settings import get_current_project_settings from openpype.pipeline import ( Anatomy, get_current_project_name, get_current_asset_name, registered_host, get_current_context, get_current_host_name, ) from openpype.pipeline.create import CreateContext from openpype.pipeline.template_data import get_template_data from openpype.pipeline.context_tools import get_current_project_asset from openpype.widgets import popup from openpype.tools.utils.host_tools import get_tool_by_name import hou self = sys.modules[__name__] self._parent = None log = logging.getLogger(__name__) JSON_PREFIX = "JSON:::" def get_asset_fps(asset_doc=None): """Return current asset fps.""" if asset_doc is None: asset_doc = get_current_project_asset(fields=["data.fps"]) return asset_doc["data"]["fps"] def set_id(node, unique_id, overwrite=False): exists = node.parm("id") if not exists: imprint(node, {"id": unique_id}) if not exists and overwrite: node.setParm("id", unique_id) def get_id(node): """Get the `cbId` attribute of the given node. Args: node (hou.Node): the name of the node to retrieve the attribute from Returns: str: cbId attribute of the node. """ if node is not None: return node.parm("id") def generate_ids(nodes, asset_id=None): """Returns new unique ids for the given nodes. Note: This does not assign the new ids, it only generates the values. To assign new ids using this method: >>> nodes = ["a", "b", "c"] >>> for node, id in generate_ids(nodes): >>> set_id(node, id) To also override any existing values (and assign regenerated ids): >>> nodes = ["a", "b", "c"] >>> for node, id in generate_ids(nodes): >>> set_id(node, id, overwrite=True) Args: nodes (list): List of nodes. asset_id (str or bson.ObjectId): The database id for the *asset* to generate for. When None provided the current asset in the active session is used. Returns: list: A list of (node, id) tuples. """ if asset_id is None: project_name = get_current_project_name() asset_name = get_current_asset_name() # Get the asset ID from the database for the asset of current context asset_doc = get_asset_by_name(project_name, asset_name, fields=["_id"]) assert asset_doc, "No current asset found in Session" asset_id = asset_doc['_id'] node_ids = [] for node in nodes: _, uid = str(uuid.uuid4()).rsplit("-", 1) unique_id = "{}:{}".format(asset_id, uid) node_ids.append((node, unique_id)) return node_ids def get_id_required_nodes(): valid_types = ["geometry"] nodes = {n for n in hou.node("/out").children() if n.type().name() in valid_types} return list(nodes) def get_output_parameter(node): """Return the render output parameter of the given node Example: root = hou.node("/obj") my_alembic_node = root.createNode("alembic") get_output_parameter(my_alembic_node) >>> "filename" Notes: I'm using node.type().name() to get on par with the creators, Because the return value of `node.type().name()` is the same string value used in creators e.g. instance_data.update({"node_type": "alembic"}) Rop nodes in different network categories have the same output parameter. So, I took that into consideration as a hint for future development. Args: node(hou.Node): node instance Returns: hou.Parm """ node_type = node.type().name() # Figure out which type of node is being rendered if node_type in {"alembic", "rop_alembic"}: return node.parm("filename") elif node_type == "arnold": if node_type.evalParm("ar_ass_export_enable"): return node.parm("ar_ass_file") return node.parm("ar_picture") elif node_type in { "geometry", "rop_geometry", "filmboxfbx", "rop_fbx" }: return node.parm("sopoutput") elif node_type == "comp": return node.parm("copoutput") elif node_type in {"karma", "opengl"}: return node.parm("picture") elif node_type == "ifd": # Mantra if node.evalParm("soho_outputmode"): return node.parm("soho_diskfile") return node.parm("vm_picture") elif node_type == "Redshift_Proxy_Output": return node.parm("RS_archive_file") elif node_type == "Redshift_ROP": return node.parm("RS_outputFileNamePrefix") elif node_type in {"usd", "usd_rop", "usdexport"}: return node.parm("lopoutput") elif node_type in {"usdrender", "usdrender_rop"}: return node.parm("outputimage") elif node_type == "vray_renderer": return node.parm("SettingsOutput_img_file_path") raise TypeError("Node type '%s' not supported" % node_type) def set_scene_fps(fps): hou.setFps(fps) # Valid FPS def validate_fps(): """Validate current scene FPS and show pop-up when it is incorrect Returns: bool """ fps = get_asset_fps() current_fps = hou.fps() # returns float if current_fps != fps: # Find main window parent = hou.ui.mainQtWindow() if parent is None: pass else: dialog = popup.PopupUpdateKeys(parent=parent) dialog.setModal(True) dialog.setWindowTitle("Houdini scene does not match project FPS") dialog.setMessage("Scene %i FPS does not match project %i FPS" % (current_fps, fps)) dialog.setButtonText("Fix") # on_show is the Fix button clicked callback dialog.on_clicked_state.connect(lambda: set_scene_fps(fps)) dialog.show() return False return True def create_remote_publish_node(force=True): """Function to create a remote publish node in /out This is a hacked "Shell" node that does *nothing* except for triggering `colorbleed.lib.publish_remote()` as pre-render script. All default attributes of the Shell node are hidden to the Artist to avoid confusion. Additionally some custom attributes are added that can be collected by a Collector to set specific settings for the publish, e.g. whether to separate the jobs per instance or process in one single job. """ cmd = "import colorbleed.lib; colorbleed.lib.publish_remote()" existing = hou.node("/out/REMOTE_PUBLISH") if existing: if force: log.warning("Removing existing '/out/REMOTE_PUBLISH' node..") existing.destroy() else: raise RuntimeError("Node already exists /out/REMOTE_PUBLISH. " "Please remove manually or set `force` to " "True.") # Create the shell node out = hou.node("/out") node = out.createNode("shell", node_name="REMOTE_PUBLISH") node.moveToGoodPosition() # Set color make it stand out (avalon/pyblish color) node.setColor(hou.Color(0.439, 0.709, 0.933)) # Set the pre-render script node.setParms({ "prerender": cmd, "lprerender": "python" # command language }) # Lock the attributes to ensure artists won't easily mess things up. node.parm("prerender").lock(True) node.parm("lprerender").lock(True) # Lock up the actual shell command command_parm = node.parm("command") command_parm.set("") command_parm.lock(True) shellexec_parm = node.parm("shellexec") shellexec_parm.set(False) shellexec_parm.lock(True) # Get the node's parm template group so we can customize it template = node.parmTemplateGroup() # Hide default tabs template.hideFolder("Shell", True) template.hideFolder("Scripts", True) # Hide default settings template.hide("execute", True) template.hide("renderdialog", True) template.hide("trange", True) template.hide("f", True) template.hide("take", True) # Add custom settings to this node. parm_folder = hou.FolderParmTemplate("folder", "Submission Settings") # Separate Jobs per Instance parm = hou.ToggleParmTemplate(name="separateJobPerInstance", label="Separate Job per Instance", default_value=False) parm_folder.addParmTemplate(parm) # Add our custom Submission Settings folder template.append(parm_folder) # Apply template back to the node node.setParmTemplateGroup(template) def render_rop(ropnode): """Render ROP node utility for Publishing. This renders a ROP node with the settings we want during Publishing. """ # Print verbose when in batch mode without UI verbose = not hou.isUIAvailable() # Render try: ropnode.render(verbose=verbose, # Allow Deadline to capture completion percentage output_progress=verbose) except hou.Error as exc: # The hou.Error is not inherited from a Python Exception class, # so we explicitly capture the houdini error, otherwise pyblish # will remain hanging. import traceback traceback.print_exc() raise RuntimeError("Render failed: {0}".format(exc)) def imprint(node, data, update=False): """Store attributes with value on a node Depending on the type of attribute it creates the correct parameter template. Houdini uses a template per type, see the docs for more information. http://www.sidefx.com/docs/houdini/hom/hou/ParmTemplate.html Because of some update glitch where you cannot overwrite existing ParmTemplates on node using: `setParmTemplates()` and `parmTuplesInFolder()` update is done in another pass. Args: node(hou.Node): node object from Houdini data(dict): collection of attributes and their value update (bool, optional): flag if imprint should update already existing data or leave them untouched and only add new. Returns: None """ if not data: return if not node: self.log.error("Node is not set, calling imprint on invalid data.") return current_parms = {p.name(): p for p in node.spareParms()} update_parm_templates = [] new_parm_templates = [] for key, value in data.items(): if value is None: continue parm_template = get_template_from_value(key, value) if key in current_parms: if node.evalParm(key) == value: continue if not update: log.debug(f"{key} already exists on {node}") else: log.debug(f"replacing {key}") update_parm_templates.append(parm_template) continue new_parm_templates.append(parm_template) if not new_parm_templates and not update_parm_templates: return parm_group = node.parmTemplateGroup() # Add new parm templates if new_parm_templates: parm_folder = parm_group.findFolder("Extra") # if folder doesn't exist yet, create one and append to it, # else append to existing one if not parm_folder: parm_folder = hou.FolderParmTemplate("folder", "Extra") parm_folder.setParmTemplates(new_parm_templates) parm_group.append(parm_folder) else: # Add to parm template folder instance then replace with updated # one in parm template group for template in new_parm_templates: parm_folder.addParmTemplate(template) parm_group.replace(parm_folder.name(), parm_folder) # Update existing parm templates for parm_template in update_parm_templates: parm_group.replace(parm_template.name(), parm_template) # When replacing a parm with a parm of the same name it preserves its # value if before the replacement the parm was not at the default, # because it has a value override set. Since we're trying to update the # parm by using the new value as `default` we enforce the parm is at # default state node.parm(parm_template.name()).revertToDefaults() node.setParmTemplateGroup(parm_group) def lsattr(attr, value=None, root="/"): """Return nodes that have `attr` When `value` is not None it will only return nodes matching that value for the given attribute. Args: attr (str): Name of the attribute (hou.Parm) value (object, Optional): The value to compare the attribute too. When the default None is provided the value check is skipped. root (str): The root path in Houdini to search in. Returns: list: Matching nodes that have attribute with value. """ if value is None: # Use allSubChildren() as allNodes() errors on nodes without # permission to enter without a means to continue of querying # the rest nodes = hou.node(root).allSubChildren() return [n for n in nodes if n.parm(attr)] return lsattrs({attr: value}) def lsattrs(attrs, root="/"): """Return nodes matching `key` and `value` Arguments: attrs (dict): collection of attribute: value root (str): The root path in Houdini to search in. Example: >> lsattrs({"id": "myId"}) ["myNode"] >> lsattr("id") ["myNode", "myOtherNode"] Returns: list: Matching nodes that have attribute with value. """ matches = set() # Use allSubChildren() as allNodes() errors on nodes without # permission to enter without a means to continue of querying # the rest nodes = hou.node(root).allSubChildren() for node in nodes: for attr in attrs: if not node.parm(attr): continue elif node.evalParm(attr) != attrs[attr]: continue else: matches.add(node) return list(matches) def read(node): """Read the container data in to a dict Args: node(hou.Node): Houdini node Returns: dict """ # `spareParms` returns a tuple of hou.Parm objects data = {} if not node: return data for parameter in node.spareParms(): value = parameter.eval() # test if value is json encoded dict if isinstance(value, six.string_types) and \ value.startswith(JSON_PREFIX): try: value = json.loads(value[len(JSON_PREFIX):]) except json.JSONDecodeError: # not a json pass data[parameter.name()] = value return data @contextmanager def maintained_selection(): """Maintain selection during context Example: >>> with maintained_selection(): ... # Modify selection ... node.setSelected(on=False, clear_all_selected=True) >>> # Selection restored """ previous_selection = hou.selectedNodes() try: yield finally: # Clear the selection # todo: does hou.clearAllSelected() do the same? for node in hou.selectedNodes(): node.setSelected(on=False) if previous_selection: for node in previous_selection: node.setSelected(on=True) def reset_framerange(): """Set frame range and FPS to current asset""" # Get asset data project_name = get_current_project_name() asset_name = get_current_asset_name() # Get the asset ID from the database for the asset of current context asset_doc = get_asset_by_name(project_name, asset_name) asset_data = asset_doc["data"] # Get FPS fps = get_asset_fps(asset_doc) # Get Start and End Frames frame_start = asset_data.get("frameStart") frame_end = asset_data.get("frameEnd") if frame_start is None or frame_end is None: log.warning("No edit information found for %s" % asset_name) return handle_start = asset_data.get("handleStart", 0) handle_end = asset_data.get("handleEnd", 0) frame_start -= int(handle_start) frame_end += int(handle_end) # Set frame range and FPS print("Setting scene FPS to {}".format(int(fps))) set_scene_fps(fps) hou.playbar.setFrameRange(frame_start, frame_end) hou.playbar.setPlaybackRange(frame_start, frame_end) hou.setFrame(frame_start) def get_main_window(): """Acquire Houdini's main window""" if self._parent is None: self._parent = hou.ui.mainQtWindow() return self._parent def get_template_from_value(key, value): if isinstance(value, float): parm = hou.FloatParmTemplate(name=key, label=key, num_components=1, default_value=(value,)) elif isinstance(value, bool): parm = hou.ToggleParmTemplate(name=key, label=key, default_value=value) elif isinstance(value, int): parm = hou.IntParmTemplate(name=key, label=key, num_components=1, default_value=(value,)) elif isinstance(value, six.string_types): parm = hou.StringParmTemplate(name=key, label=key, num_components=1, default_value=(value,)) elif isinstance(value, (dict, list, tuple)): parm = hou.StringParmTemplate(name=key, label=key, num_components=1, default_value=( JSON_PREFIX + json.dumps(value),)) else: raise TypeError("Unsupported type: %r" % type(value)) return parm def get_frame_data(node, log=None): """Get the frame data: `frameStartHandle`, `frameEndHandle` and `byFrameStep`. This function uses Houdini node's `trange`, `t1, `t2` and `t3` parameters as the source of truth for the full inclusive frame range to render, as such these are considered as the frame range including the handles. The non-inclusive frame start and frame end without handles can be computed by subtracting the handles from the inclusive frame range. Args: node (hou.Node): ROP node to retrieve frame range from, the frame range is assumed to be the frame range *including* the start and end handles. Returns: dict: frame data for `frameStartHandle`, `frameEndHandle` and `byFrameStep`. """ if log is None: log = self.log data = {} if node.parm("trange") is None: log.debug( "Node has no 'trange' parameter: {}".format(node.path()) ) return data if node.evalParm("trange") == 0: data["frameStartHandle"] = hou.intFrame() data["frameEndHandle"] = hou.intFrame() data["byFrameStep"] = 1.0 log.info( "Node '{}' has 'Render current frame' set.\n" "Asset Handles are ignored.\n" "frameStart and frameEnd are set to the " "current frame.".format(node.path()) ) else: data["frameStartHandle"] = int(node.evalParm("f1")) data["frameEndHandle"] = int(node.evalParm("f2")) data["byFrameStep"] = node.evalParm("f3") return data def splitext(name, allowed_multidot_extensions): # type: (str, list) -> tuple """Split file name to name and extension. Args: name (str): File name to split. allowed_multidot_extensions (list of str): List of allowed multidot extensions. Returns: tuple: Name and extension. """ for ext in allowed_multidot_extensions: if name.endswith(ext): return name[:-len(ext)], ext return os.path.splitext(name) def get_top_referenced_parm(parm): processed = set() # disallow infinite loop while True: if parm.path() in processed: raise RuntimeError("Parameter references result in cycle.") processed.add(parm.path()) ref = parm.getReferencedParm() if ref.path() == parm.path(): # It returns itself when it doesn't reference # another parameter return ref else: parm = ref def evalParmNoFrame(node, parm, pad_character="#"): parameter = node.parm(parm) assert parameter, "Parameter does not exist: %s.%s" % (node, parm) # If the parameter has a parameter reference, then get that # parameter instead as otherwise `unexpandedString()` fails. parameter = get_top_referenced_parm(parameter) # Substitute out the frame numbering with padded characters try: raw = parameter.unexpandedString() except hou.Error as exc: print("Failed: %s" % parameter) raise RuntimeError(exc) def replace(match): padding = 1 n = match.group(2) if n and int(n): padding = int(n) return pad_character * padding expression = re.sub(r"(\$F([0-9]*))", replace, raw) with hou.ScriptEvalContext(parameter): return hou.expandStringAtFrame(expression, 0) def get_color_management_preferences(): """Get default OCIO preferences""" return { "config": hou.Color.ocio_configPath(), "display": hou.Color.ocio_defaultDisplay(), "view": hou.Color.ocio_defaultView() } def get_obj_node_output(obj_node): """Find output node. If the node has any output node return the output node with the minimum `outputidx`. When no output is present return the node with the display flag set. If no output node is detected then None is returned. Arguments: node (hou.Node): The node to retrieve a single the output node for. Returns: Optional[hou.Node]: The child output node. """ outputs = obj_node.subnetOutputs() if not outputs: return elif len(outputs) == 1: return outputs[0] else: return min(outputs, key=lambda node: node.evalParm('outputidx')) def get_output_children(output_node, include_sops=True): """Recursively return a list of all output nodes contained in this node including this node. It works in a similar manner to output_node.allNodes(). """ out_list = [output_node] if output_node.childTypeCategory() == hou.objNodeTypeCategory(): for child in output_node.children(): out_list += get_output_children(child, include_sops=include_sops) elif include_sops and \ output_node.childTypeCategory() == hou.sopNodeTypeCategory(): out = get_obj_node_output(output_node) if out: out_list += [out] return out_list def get_resolution_from_doc(doc): """Get resolution from the given asset document. """ if not doc or "data" not in doc: print("Entered document is not valid. \"{}\"".format(str(doc))) return None resolution_width = doc["data"].get("resolutionWidth") resolution_height = doc["data"].get("resolutionHeight") # Make sure both width and height are set if resolution_width is None or resolution_height is None: print("No resolution information found for \"{}\"".format(doc["name"])) return None return int(resolution_width), int(resolution_height) def set_camera_resolution(camera, asset_doc=None): """Apply resolution to camera from asset document of the publish""" if not asset_doc: asset_doc = get_current_project_asset() resolution = get_resolution_from_doc(asset_doc) if resolution: print("Setting camera resolution: {} -> {}x{}".format( camera.name(), resolution[0], resolution[1] )) camera.parm("resx").set(resolution[0]) camera.parm("resy").set(resolution[1]) def get_camera_from_container(container): """Get camera from container node. """ cameras = container.recursiveGlob( "*", filter=hou.nodeTypeFilter.ObjCamera, include_subnets=False ) assert len(cameras) == 1, "Camera instance must have only one camera" return cameras[0] def get_current_context_template_data_with_asset_data(): """ TODOs: Support both 'assetData' and 'folderData' in future. """ context = get_current_context() project_name = context["project_name"] asset_name = context["asset_name"] task_name = context["task_name"] host_name = get_current_host_name() anatomy = Anatomy(project_name) project_doc = get_project(project_name) asset_doc = get_asset_by_name(project_name, asset_name) # get context specific vars asset_data = asset_doc["data"] # compute `frameStartHandle` and `frameEndHandle` frame_start = asset_data.get("frameStart") frame_end = asset_data.get("frameEnd") handle_start = asset_data.get("handleStart") handle_end = asset_data.get("handleEnd") if frame_start is not None and handle_start is not None: asset_data["frameStartHandle"] = frame_start - handle_start if frame_end is not None and handle_end is not None: asset_data["frameEndHandle"] = frame_end + handle_end template_data = get_template_data( project_doc, asset_doc, task_name, host_name ) template_data["root"] = anatomy.roots template_data["assetData"] = asset_data return template_data def get_context_var_changes(): """get context var changes.""" houdini_vars_to_update = {} project_settings = get_current_project_settings() houdini_vars_settings = \ project_settings["houdini"]["general"]["update_houdini_var_context"] if not houdini_vars_settings["enabled"]: return houdini_vars_to_update houdini_vars = houdini_vars_settings["houdini_vars"] # No vars specified - nothing to do if not houdini_vars: return houdini_vars_to_update # Get Template data template_data = get_current_context_template_data_with_asset_data() # Set Houdini Vars for item in houdini_vars: # For consistency reasons we always force all vars to be uppercase # Also remove any leading, and trailing whitespaces. var = item["var"].strip().upper() # get and resolve template in value item_value = StringTemplate.format_template( item["value"], template_data ) if var == "JOB" and item_value == "": # sync $JOB to $HIP if $JOB is empty item_value = os.environ["HIP"] if item["is_directory"]: item_value = item_value.replace("\\", "/") current_value = hou.hscript("echo -n `${}`".format(var))[0] if current_value != item_value: houdini_vars_to_update[var] = ( current_value, item_value, item["is_directory"] ) return houdini_vars_to_update def update_houdini_vars_context(): """Update asset context variables""" for var, (_old, new, is_directory) in get_context_var_changes().items(): if is_directory: try: os.makedirs(new) except OSError as e: if e.errno != errno.EEXIST: print( "Failed to create ${} dir. Maybe due to " "insufficient permissions.".format(var) ) hou.hscript("set {}={}".format(var, new)) os.environ[var] = new print("Updated ${} to {}".format(var, new)) def update_houdini_vars_context_dialog(): """Show pop-up to update asset context variables""" update_vars = get_context_var_changes() if not update_vars: # Nothing to change print("Nothing to change, Houdini vars are already up to date.") return message = "\n".join( "${}: {} -> {}".format(var, old or "None", new or "None") for var, (old, new, _is_directory) in update_vars.items() ) # TODO: Use better UI! parent = hou.ui.mainQtWindow() dialog = popup.Popup(parent=parent) dialog.setModal(True) dialog.setWindowTitle("Houdini scene has outdated asset variables") dialog.setMessage(message) dialog.setButtonText("Fix") # on_show is the Fix button clicked callback dialog.on_clicked.connect(update_houdini_vars_context) dialog.show() def publisher_show_and_publish(comment=None): """Open publisher window and trigger publishing action. Args: comment (Optional[str]): Comment to set in publisher window. """ main_window = get_main_window() publisher_window = get_tool_by_name( tool_name="publisher", parent=main_window, ) publisher_window.show_and_publish(comment) def find_rop_input_dependencies(input_tuple): """Self publish from ROP nodes. Arguments: tuple (hou.RopNode.inputDependencies) which can be a nested tuples represents the input dependencies of the ROP node, consisting of ROPs, and the frames that need to be be rendered prior to rendering the ROP. Returns: list of the RopNode.path() that can be found inside the input tuple. """ out_list = [] if isinstance(input_tuple[0], hou.RopNode): return input_tuple[0].path() if isinstance(input_tuple[0], tuple): for item in input_tuple: out_list.append(find_rop_input_dependencies(item)) return out_list def self_publish(): """Self publish from ROP nodes. Firstly, it gets the node and its dependencies. Then, it deactivates all other ROPs And finaly, it triggers the publishing action. """ result, comment = hou.ui.readInput( "Add Publish Comment", buttons=("Publish", "Cancel"), title="Publish comment", close_choice=1 ) if result: return current_node = hou.node(".") inputs_paths = find_rop_input_dependencies( current_node.inputDependencies() ) inputs_paths.append(current_node.path()) host = registered_host() context = CreateContext(host, reset=True) for instance in context.instances: node_path = instance.data.get("instance_node") instance["active"] = node_path and node_path in inputs_paths context.save_changes() publisher_show_and_publish(comment) def add_self_publish_button(node): """Adds a self publish button to the rop node.""" label = os.environ.get("AVALON_LABEL") or "AYON" button_parm = hou.ButtonParmTemplate( "ayon_self_publish", "{} Publish".format(label), script_callback="from openpype.hosts.houdini.api.lib import " "self_publish; self_publish()", script_callback_language=hou.scriptLanguage.Python, join_with_next=True ) template = node.parmTemplateGroup() template.insertBefore((0,), button_parm) node.setParmTemplateGroup(template) ================================================ FILE: openpype/hosts/houdini/api/pipeline.py ================================================ # -*- coding: utf-8 -*- """Pipeline tools for OpenPype Houdini integration.""" import os import sys import logging import hou # noqa from openpype.host import HostBase, IWorkfileHost, ILoadHost, IPublishHost import pyblish.api from openpype.pipeline import ( register_creator_plugin_path, register_loader_plugin_path, register_inventory_action_path, AVALON_CONTAINER_ID, ) from openpype.pipeline.load import any_outdated_containers from openpype.hosts.houdini import HOUDINI_HOST_DIR from openpype.hosts.houdini.api import lib, shelves, creator_node_shelves from openpype.lib import ( register_event_callback, emit_event, ) log = logging.getLogger("openpype.hosts.houdini") AVALON_CONTAINERS = "/obj/AVALON_CONTAINERS" CONTEXT_CONTAINER = "/obj/OpenPypeContext" IS_HEADLESS = not hasattr(hou, "ui") PLUGINS_DIR = os.path.join(HOUDINI_HOST_DIR, "plugins") PUBLISH_PATH = os.path.join(PLUGINS_DIR, "publish") LOAD_PATH = os.path.join(PLUGINS_DIR, "load") CREATE_PATH = os.path.join(PLUGINS_DIR, "create") INVENTORY_PATH = os.path.join(PLUGINS_DIR, "inventory") class HoudiniHost(HostBase, IWorkfileHost, ILoadHost, IPublishHost): name = "houdini" def __init__(self): super(HoudiniHost, self).__init__() self._op_events = {} self._has_been_setup = False def install(self): pyblish.api.register_host("houdini") pyblish.api.register_host("hython") pyblish.api.register_host("hpython") pyblish.api.register_plugin_path(PUBLISH_PATH) register_loader_plugin_path(LOAD_PATH) register_creator_plugin_path(CREATE_PATH) register_inventory_action_path(INVENTORY_PATH) log.info("Installing callbacks ... ") # register_event_callback("init", on_init) self._register_callbacks() register_event_callback("before.save", before_save) register_event_callback("save", on_save) register_event_callback("open", on_open) register_event_callback("new", on_new) self._has_been_setup = True # Set asset settings for the empty scene directly after launch of # Houdini so it initializes into the correct scene FPS, # Frame Range, etc. # TODO: make sure this doesn't trigger when # opening with last workfile. _set_context_settings() if not IS_HEADLESS: import hdefereval # noqa, hdefereval is only available in ui mode # Defer generation of shelves due to issue on Windows where shelf # initialization during start up delays Houdini UI by minutes # making it extremely slow to launch. hdefereval.executeDeferred(shelves.generate_shelves) if not IS_HEADLESS: import hdefereval # noqa, hdefereval is only available in ui mode hdefereval.executeDeferred(creator_node_shelves.install) def workfile_has_unsaved_changes(self): return hou.hipFile.hasUnsavedChanges() def get_workfile_extensions(self): return [".hip", ".hiplc", ".hipnc"] def save_workfile(self, dst_path=None): # Force forwards slashes to avoid segfault if dst_path: dst_path = dst_path.replace("\\", "/") hou.hipFile.save(file_name=dst_path, save_to_recent_files=True) return dst_path def open_workfile(self, filepath): # Force forwards slashes to avoid segfault filepath = filepath.replace("\\", "/") hou.hipFile.load(filepath, suppress_save_prompt=True, ignore_load_warnings=False) return filepath def get_current_workfile(self): current_filepath = hou.hipFile.path() if (os.path.basename(current_filepath) == "untitled.hip" and not os.path.exists(current_filepath)): # By default a new scene in houdini is saved in the current # working directory as "untitled.hip" so we need to capture # that and consider it 'not saved' when it's in that state. return None return current_filepath def get_containers(self): return ls() def _register_callbacks(self): for event in self._op_events.copy().values(): if event is None: continue try: hou.hipFile.removeEventCallback(event) except RuntimeError as e: log.info(e) self._op_events[on_file_event_callback] = hou.hipFile.addEventCallback( on_file_event_callback ) @staticmethod def create_context_node(): """Helper for creating context holding node. Returns: hou.Node: context node """ obj_network = hou.node("/obj") op_ctx = obj_network.createNode("subnet", node_name="OpenPypeContext", run_init_scripts=False, load_contents=False) op_ctx.moveToGoodPosition() op_ctx.setBuiltExplicitly(False) op_ctx.setCreatorState("OpenPype") op_ctx.setComment("OpenPype node to hold context metadata") op_ctx.setColor(hou.Color((0.081, 0.798, 0.810))) op_ctx.setDisplayFlag(False) op_ctx.hide(True) return op_ctx def update_context_data(self, data, changes): op_ctx = hou.node(CONTEXT_CONTAINER) if not op_ctx: op_ctx = self.create_context_node() lib.imprint(op_ctx, data) def get_context_data(self): op_ctx = hou.node(CONTEXT_CONTAINER) if not op_ctx: op_ctx = self.create_context_node() return lib.read(op_ctx) def save_file(self, dst_path=None): # Force forwards slashes to avoid segfault dst_path = dst_path.replace("\\", "/") hou.hipFile.save(file_name=dst_path, save_to_recent_files=True) def on_file_event_callback(event): if event == hou.hipFileEventType.AfterLoad: emit_event("open") elif event == hou.hipFileEventType.AfterSave: emit_event("save") elif event == hou.hipFileEventType.BeforeSave: emit_event("before.save") elif event == hou.hipFileEventType.AfterClear: emit_event("new") def containerise(name, namespace, nodes, context, loader=None, suffix=""): """Bundle `nodes` into a subnet and imprint it with metadata Containerisation enables a tracking of version, author and origin for loaded assets. Arguments: name (str): Name of resulting assembly namespace (str): Namespace under which to host container nodes (list): Long names of nodes to containerise context (dict): Asset information loader (str, optional): Name of loader used to produce this container. suffix (str, optional): Suffix of container, defaults to `_CON`. Returns: container (str): Name of container assembly """ # Ensure AVALON_CONTAINERS subnet exists subnet = hou.node(AVALON_CONTAINERS) if subnet is None: obj_network = hou.node("/obj") subnet = obj_network.createNode("subnet", node_name="AVALON_CONTAINERS") # Create proper container name container_name = "{}_{}".format(name, suffix or "CON") container = hou.node("/obj/{}".format(name)) container.setName(container_name, unique_name=True) data = { "schema": "openpype:container-2.0", "id": AVALON_CONTAINER_ID, "name": name, "namespace": namespace, "loader": str(loader), "representation": str(context["representation"]["_id"]), } lib.imprint(container, data) # "Parent" the container under the container network hou.moveNodesTo([container], subnet) subnet.node(container_name).moveToGoodPosition() return container def parse_container(container): """Return the container node's full container data. Args: container (hou.Node): A container node name. Returns: dict: The container schema data for this container node. """ data = lib.read(container) # Backwards compatibility pre-schemas for containers data["schema"] = data.get("schema", "openpype:container-1.0") # Append transient data data["objectName"] = container.path() data["node"] = container return data def ls(): containers = [] for identifier in (AVALON_CONTAINER_ID, "pyblish.mindbender.container"): containers += lib.lsattr("id", identifier) for container in sorted(containers, # Hou 19+ Python 3 hou.ObjNode are not # sortable due to not supporting greater # than comparisons key=lambda node: node.path()): yield parse_container(container) def before_save(): return lib.validate_fps() def on_save(): log.info("Running callback on save..") # update houdini vars lib.update_houdini_vars_context_dialog() nodes = lib.get_id_required_nodes() for node, new_id in lib.generate_ids(nodes): lib.set_id(node, new_id, overwrite=False) def _show_outdated_content_popup(): # Get main window parent = lib.get_main_window() if parent is None: log.info("Skipping outdated content pop-up " "because Houdini window can't be found.") else: from openpype.widgets import popup # Show outdated pop-up def _on_show_inventory(): from openpype.tools.utils import host_tools host_tools.show_scene_inventory(parent=parent) dialog = popup.Popup(parent=parent) dialog.setWindowTitle("Houdini scene has outdated content") dialog.setMessage("There are outdated containers in " "your Houdini scene.") dialog.on_clicked.connect(_on_show_inventory) dialog.show() def on_open(): if not hou.isUIAvailable(): log.debug("Batch mode detected, ignoring `on_open` callbacks..") return log.info("Running callback on open..") # update houdini vars lib.update_houdini_vars_context_dialog() # Validate FPS after update_task_from_path to # ensure it is using correct FPS for the asset lib.validate_fps() if any_outdated_containers(): parent = lib.get_main_window() if parent is None: # When opening Houdini with last workfile on launch the UI hasn't # initialized yet completely when the `on_open` callback triggers. # We defer the dialog popup to wait for the UI to become available. # We assume it will open because `hou.isUIAvailable()` returns True import hdefereval hdefereval.executeDeferred(_show_outdated_content_popup) else: _show_outdated_content_popup() log.warning("Scene has outdated content.") def on_new(): """Set project resolution and fps when create a new file""" if hou.hipFile.isLoadingHipFile(): # This event also triggers when Houdini opens a file due to the # new event being registered to 'afterClear'. As such we can skip # 'new' logic if the user is opening a file anyway log.debug("Skipping on new callback due to scene being opened.") return log.info("Running callback on new..") _set_context_settings() # It seems that the current frame always gets reset to frame 1 on # new scene. So we enforce current frame to be at the start of the playbar # with execute deferred def _enforce_start_frame(): start = hou.playbar.playbackRange()[0] hou.setFrame(start) if hou.isUIAvailable(): import hdefereval hdefereval.executeDeferred(_enforce_start_frame) else: # Run without execute deferred when no UI is available because # without UI `hdefereval` is not available to import _enforce_start_frame() def _set_context_settings(): """Apply the project settings from the project definition Settings can be overwritten by an asset if the asset.data contains any information regarding those settings. Examples of settings: fps resolution renderer Returns: None """ lib.reset_framerange() lib.update_houdini_vars_context() ================================================ FILE: openpype/hosts/houdini/api/plugin.py ================================================ # -*- coding: utf-8 -*- """Houdini specific Avalon/Pyblish plugin definitions.""" import sys from abc import ( ABCMeta ) import six import hou from openpype import AYON_SERVER_ENABLED from openpype.pipeline import ( CreatorError, LegacyCreator, Creator as NewCreator, CreatedInstance ) from openpype.lib import BoolDef from .lib import imprint, read, lsattr, add_self_publish_button class OpenPypeCreatorError(CreatorError): pass class Creator(LegacyCreator): """Creator plugin to create instances in Houdini To support the wide range of node types for render output (Alembic, VDB, Mantra) the Creator needs a node type to create the correct instance By default, if none is given, is `geometry`. An example of accepted node types: geometry, alembic, ifd (mantra) Please check the Houdini documentation for more node types. Tip: to find the exact node type to create press the `i` left of the node when hovering over a node. The information is visible under the name of the node. Deprecated: This creator is deprecated and will be removed in future version. """ defaults = ['Main'] def __init__(self, *args, **kwargs): super(Creator, self).__init__(*args, **kwargs) self.nodes = [] def process(self): """This is the base functionality to create instances in Houdini The selected nodes are stored in self to be used in an override method. This is currently necessary in order to support the multiple output types in Houdini which can only be rendered through their own node. Default node type if none is given is `geometry` It also makes it easier to apply custom settings per instance type Example of override method for Alembic: def process(self): instance = super(CreateEpicNode, self, process() # Set parameters for Alembic node instance.setParms( {"sop_path": "$HIP/%s.abc" % self.nodes[0]} ) Returns: hou.Node """ try: if (self.options or {}).get("useSelection"): self.nodes = hou.selectedNodes() # Get the node type and remove it from the data, not needed node_type = self.data.pop("node_type", None) if node_type is None: node_type = "geometry" # Get out node out = hou.node("/out") instance = out.createNode(node_type, node_name=self.name) instance.moveToGoodPosition() imprint(instance, self.data) self._process(instance) except hou.Error as er: six.reraise( OpenPypeCreatorError, OpenPypeCreatorError("Creator error: {}".format(er)), sys.exc_info()[2]) class HoudiniCreatorBase(object): @staticmethod def cache_subsets(shared_data): """Cache instances for Creators to shared data. Create `houdini_cached_subsets` key when needed in shared data and fill it with all collected instances from the scene under its respective creator identifiers. Create `houdini_cached_legacy_subsets` key for any legacy instances detected in the scene as instances per family. Args: Dict[str, Any]: Shared data. Return: Dict[str, Any]: Shared data dictionary. """ if shared_data.get("houdini_cached_subsets") is None: cache = dict() cache_legacy = dict() for node in lsattr("id", "pyblish.avalon.instance"): creator_identifier_parm = node.parm("creator_identifier") if creator_identifier_parm: # creator instance creator_id = creator_identifier_parm.eval() cache.setdefault(creator_id, []).append(node) else: # legacy instance family_parm = node.parm("family") if not family_parm: # must be a broken instance continue family = family_parm.eval() cache_legacy.setdefault(family, []).append(node) shared_data["houdini_cached_subsets"] = cache shared_data["houdini_cached_legacy_subsets"] = cache_legacy return shared_data @staticmethod def create_instance_node( asset_name, node_name, parent, node_type="geometry" ): # type: (str, str, str) -> hou.Node """Create node representing instance. Arguments: asset_name (str): Asset name. node_name (str): Name of the new node. parent (str): Name of the parent node. node_type (str, optional): Type of the node. Returns: hou.Node: Newly created instance node. """ parent_node = hou.node(parent) instance_node = parent_node.createNode( node_type, node_name=node_name) instance_node.moveToGoodPosition() return instance_node @six.add_metaclass(ABCMeta) class HoudiniCreator(NewCreator, HoudiniCreatorBase): """Base class for most of the Houdini creator plugins.""" selected_nodes = [] settings_name = None add_publish_button = False def create(self, subset_name, instance_data, pre_create_data): try: self.selected_nodes = [] if pre_create_data.get("use_selection"): self.selected_nodes = hou.selectedNodes() # Get the node type and remove it from the data, not needed node_type = instance_data.pop("node_type", None) if node_type is None: node_type = "geometry" if AYON_SERVER_ENABLED: asset_name = instance_data["folderPath"] else: asset_name = instance_data["asset"] instance_node = self.create_instance_node( asset_name, subset_name, "/out", node_type) self.customize_node_look(instance_node) instance_data["instance_node"] = instance_node.path() instance_data["instance_id"] = instance_node.path() instance = CreatedInstance( self.family, subset_name, instance_data, self) self._add_instance_to_context(instance) self.imprint(instance_node, instance.data_to_store()) if self.add_publish_button: add_self_publish_button(instance_node) return instance except hou.Error as er: six.reraise( OpenPypeCreatorError, OpenPypeCreatorError("Creator error: {}".format(er)), sys.exc_info()[2]) def lock_parameters(self, node, parameters): """Lock list of specified parameters on the node. Args: node (hou.Node): Houdini node to lock parameters on. parameters (list of str): List of parameter names. """ for name in parameters: try: parm = node.parm(name) parm.lock(True) except AttributeError: self.log.debug("missing lock pattern {}".format(name)) def collect_instances(self): # cache instances if missing self.cache_subsets(self.collection_shared_data) for instance in self.collection_shared_data[ "houdini_cached_subsets"].get(self.identifier, []): node_data = read(instance) # Node paths are always the full node path since that is unique # Because it's the node's path it's not written into attributes # but explicitly collected node_path = instance.path() node_data["instance_id"] = node_path node_data["instance_node"] = node_path created_instance = CreatedInstance.from_existing( node_data, self ) self._add_instance_to_context(created_instance) def update_instances(self, update_list): for created_inst, changes in update_list: instance_node = hou.node(created_inst.get("instance_node")) new_values = { key: changes[key].new_value for key in changes.changed_keys } # Update parm templates and values self.imprint( instance_node, new_values, update=True ) def imprint(self, node, values, update=False): # Never store instance node and instance id since that data comes # from the node's path values.pop("instance_node", None) values.pop("instance_id", None) imprint(node, values, update=update) def remove_instances(self, instances): """Remove specified instance from the scene. This is only removing `id` parameter so instance is no longer instance, because it might contain valuable data for artist. """ for instance in instances: instance_node = hou.node(instance.data.get("instance_node")) if instance_node: instance_node.destroy() self._remove_instance_from_context(instance) def get_pre_create_attr_defs(self): return [ BoolDef("use_selection", label="Use selection") ] @staticmethod def customize_node_look( node, color=None, shape="chevron_down"): """Set custom look for instance nodes. Args: node (hou.Node): Node to set look. color (hou.Color, Optional): Color of the node. shape (str, Optional): Shape name of the node. Returns: None """ if not color: color = hou.Color((0.616, 0.871, 0.769)) node.setUserData('nodeshape', shape) node.setColor(color) def get_network_categories(self): """Return in which network view type this creator should show. The node type categories returned here will be used to define where the creator will show up in the TAB search for nodes in Houdini's Network View. This can be overridden in inherited classes to define where that particular Creator should be visible in the TAB search. Returns: list: List of houdini node type categories """ return [hou.ropNodeTypeCategory()] def apply_settings(self, project_settings): """Method called on initialization of plugin to apply settings.""" # Apply General Settings houdini_general_settings = project_settings["houdini"]["general"] self.add_publish_button = houdini_general_settings.get( "add_self_publish_button", False) # Apply Creator Settings settings_name = self.settings_name if settings_name is None: settings_name = self.__class__.__name__ settings = project_settings["houdini"]["create"] settings = settings.get(settings_name) if settings is None: self.log.debug( "No settings found for {}".format(self.__class__.__name__) ) return for key, value in settings.items(): setattr(self, key, value) ================================================ FILE: openpype/hosts/houdini/api/shelves.py ================================================ import os import re import logging import platform from openpype.settings import get_project_settings from openpype.pipeline import get_current_project_name from openpype.lib import StringTemplate import hou from .lib import get_current_context_template_data_with_asset_data log = logging.getLogger("openpype.hosts.houdini.shelves") def generate_shelves(): """This function generates complete shelves from shelf set to tools in Houdini from openpype project settings houdini shelf definition. """ current_os = platform.system().lower() # load configuration of houdini shelves project_name = get_current_project_name() project_settings = get_project_settings(project_name) shelves_configs = project_settings["houdini"]["shelves"] if not shelves_configs: log.debug("No custom shelves found in project settings.") return # Get Template data template_data = get_current_context_template_data_with_asset_data() for config in shelves_configs: selected_option = config["options"] shelf_set_config = config[selected_option] shelf_set_filepath = shelf_set_config.get('shelf_set_source_path') if shelf_set_filepath: shelf_set_os_filepath = shelf_set_filepath[current_os] if shelf_set_os_filepath: shelf_set_os_filepath = get_path_using_template_data( shelf_set_os_filepath, template_data ) if not os.path.isfile(shelf_set_os_filepath): log.error("Shelf path doesn't exist - " "{}".format(shelf_set_os_filepath)) continue hou.shelves.loadFile(shelf_set_os_filepath) continue shelf_set_name = shelf_set_config.get('shelf_set_name') if not shelf_set_name: log.warning("No name found in shelf set definition.") continue shelves_definition = shelf_set_config.get('shelf_definition') if not shelves_definition: log.debug( "No shelf definition found for shelf set named '{}'".format( shelf_set_name ) ) continue shelf_set = get_or_create_shelf_set(shelf_set_name) for shelf_definition in shelves_definition: shelf_name = shelf_definition.get('shelf_name') if not shelf_name: log.warning("No name found in shelf definition.") continue shelf = get_or_create_shelf(shelf_name) if not shelf_definition.get('tools_list'): log.debug( "No tool definition found for shelf named {}".format( shelf_name ) ) continue mandatory_attributes = {'label', 'script'} for tool_definition in shelf_definition.get('tools_list'): # We verify that the name and script attributes of the tool # are set if not all( tool_definition[key] for key in mandatory_attributes ): log.warning( "You need to specify at least the name and the " "script path of the tool.") continue tool = get_or_create_tool( tool_definition, shelf, template_data ) if not tool: continue # Add the tool to the shelf if not already in it if tool not in shelf.tools(): shelf.setTools(list(shelf.tools()) + [tool]) # Add the shelf in the shelf set if not already in it if shelf not in shelf_set.shelves(): shelf_set.setShelves(shelf_set.shelves() + (shelf,)) def get_or_create_shelf_set(shelf_set_label): """This function verifies if the shelf set label exists. If not, creates a new shelf set. Arguments: shelf_set_label (str): The label of the shelf set Returns: hou.ShelfSet: The shelf set existing or the new one """ all_shelves_sets = hou.shelves.shelfSets().values() shelf_set = next((shelf for shelf in all_shelves_sets if shelf.label() == shelf_set_label), None) if shelf_set: return shelf_set shelf_set_name = shelf_set_label.replace(' ', '_').lower() new_shelf_set = hou.shelves.newShelfSet( name=shelf_set_name, label=shelf_set_label ) return new_shelf_set def get_or_create_shelf(shelf_label): """This function verifies if the shelf label exists. If not, creates a new shelf. Arguments: shelf_label (str): The label of the shelf Returns: hou.Shelf: The shelf existing or the new one """ all_shelves = hou.shelves.shelves().values() shelf = next((s for s in all_shelves if s.label() == shelf_label), None) if shelf: return shelf shelf_name = shelf_label.replace(' ', '_').lower() new_shelf = hou.shelves.newShelf( name=shelf_name, label=shelf_label ) return new_shelf def get_or_create_tool(tool_definition, shelf, template_data): """This function verifies if the tool exists and updates it. If not, creates a new one. Arguments: tool_definition (dict): Dict with label, script, icon and help shelf (hou.Shelf): The parent shelf of the tool Returns: hou.Tool: The tool updated or the new one """ tool_label = tool_definition.get("label") if not tool_label: log.warning("Skipped shelf without label") return script_path = tool_definition["script"] script_path = get_path_using_template_data(script_path, template_data) if not script_path or not os.path.exists(script_path): log.warning("This path doesn't exist - {}".format(script_path)) return icon_path = tool_definition["icon"] if icon_path: icon_path = get_path_using_template_data(icon_path, template_data) tool_definition["icon"] = icon_path existing_tools = shelf.tools() existing_tool = next( (tool for tool in existing_tools if tool.label() == tool_label), None ) with open(script_path) as stream: script = stream.read() tool_definition["script"] = script if existing_tool: tool_definition.pop("label", None) existing_tool.setData(**tool_definition) return existing_tool tool_name = re.sub(r"[^\w\d]+", "_", tool_label).lower() return hou.shelves.newTool(name=tool_name, **tool_definition) def get_path_using_template_data(path, template_data): path = StringTemplate.format_template(path, template_data) path = path.replace("\\", "/") return path ================================================ FILE: openpype/hosts/houdini/api/usd.py ================================================ """Houdini-specific USD Library functions.""" import contextlib import logging from qtpy import QtWidgets, QtCore, QtGui from openpype import style from openpype.client import get_asset_by_name from openpype.pipeline import legacy_io from openpype.tools.utils.assets_widget import SingleSelectAssetsWidget from pxr import Sdf log = logging.getLogger(__name__) class SelectAssetDialog(QtWidgets.QWidget): """Frameless assets dialog to select asset with double click. Args: parm: Parameter where selected asset name is set. """ def __init__(self, parm): self.setWindowTitle("Pick Asset") self.setWindowFlags(QtCore.Qt.FramelessWindowHint | QtCore.Qt.Popup) assets_widget = SingleSelectAssetsWidget(legacy_io, parent=self) layout = QtWidgets.QHBoxLayout(self) layout.addWidget(assets_widget) assets_widget.double_clicked.connect(self._set_parameter) self._assets_widget = assets_widget self._parm = parm def _set_parameter(self): name = self._assets_widget.get_selected_asset_name() self._parm.set(name) self.close() def _on_show(self): pos = QtGui.QCursor.pos() # Select the current asset if there is any select_id = None name = self._parm.eval() if name: project_name = legacy_io.active_project() db_asset = get_asset_by_name(project_name, name, fields=["_id"]) if db_asset: select_id = db_asset["_id"] # Set stylesheet self.setStyleSheet(style.load_stylesheet()) # Refresh assets (is threaded) self._assets_widget.refresh() # Select asset - must be done after refresh if select_id is not None: self._assets_widget.select_asset(select_id) # Show cursor (top right of window) near cursor self.resize(250, 400) self.move(self.mapFromGlobal(pos) - QtCore.QPoint(self.width(), 0)) def showEvent(self, event): super(SelectAssetDialog, self).showEvent(event) self._on_show() def pick_asset(node): """Show a user interface to select an Asset in the project When double clicking an asset it will set the Asset value in the 'asset' parameter. """ parm = node.parm("asset_name") if not parm: log.error("Node has no 'asset' parameter: %s", node) return # Construct a frameless popup so it automatically # closes when clicked outside of it. global tool tool = SelectAssetDialog(parm) tool.show() def add_usd_output_processor(ropnode, processor): """Add USD Output Processor to USD Rop node. Args: ropnode (hou.RopNode): The USD Rop node. processor (str): The output processor name. This is the basename of the python file that contains the Houdini USD Output Processor. """ import loputils loputils.handleOutputProcessorAdd( { "node": ropnode, "parm": ropnode.parm("outputprocessors"), "script_value": processor, } ) def remove_usd_output_processor(ropnode, processor): """Removes USD Output Processor from USD Rop node. Args: ropnode (hou.RopNode): The USD Rop node. processor (str): The output processor name. This is the basename of the python file that contains the Houdini USD Output Processor. """ import loputils parm = ropnode.parm(processor + "_remove") if not parm: raise RuntimeError( "Output Processor %s does not " "exist on %s" % (processor, ropnode.name()) ) loputils.handleOutputProcessorRemove({"node": ropnode, "parm": parm}) @contextlib.contextmanager def outputprocessors(ropnode, processors=tuple(), disable_all_others=True): """Context manager to temporarily add Output Processors to USD ROP node. Args: ropnode (hou.RopNode): The USD Rop node. processors (tuple or list): The processors to add. disable_all_others (bool, Optional): Whether to disable all output processors currently on the ROP node that are not in the `processors` list passed to this function. """ # TODO: Add support for forcing the correct Order of the processors original = [] prefix = "enableoutputprocessor_" processor_parms = ropnode.globParms(prefix + "*") for parm in processor_parms: original.append((parm, parm.eval())) if disable_all_others: for parm in processor_parms: parm.set(False) added = [] for processor in processors: parm = ropnode.parm(prefix + processor) if parm: # If processor already exists, just enable it parm.set(True) else: # Else add the new processor add_usd_output_processor(ropnode, processor) added.append(processor) try: yield finally: # Remove newly added processors for processor in added: remove_usd_output_processor(ropnode, processor) # Revert to original values for parm, value in original: if parm: parm.set(value) def get_usd_rop_loppath(node): # Get sop path node_type = node.type().name() if node_type == "usd": return node.parm("loppath").evalAsNode() elif node_type in {"usd_rop", "usdrender_rop"}: # Inside Solaris e.g. /stage (not in ROP context) # When incoming connection is present it takes it directly inputs = node.inputs() if inputs: return inputs[0] else: return node.parm("loppath").evalAsNode() def get_layer_save_path(layer): """Get custom HoudiniLayerInfo->HoudiniSavePath from SdfLayer. Args: layer (pxr.Sdf.Layer): The Layer to retrieve the save pah data from. Returns: str or None: Path to save to when data exists. """ hou_layer_info = layer.rootPrims.get("HoudiniLayerInfo") if not hou_layer_info: return save_path = hou_layer_info.customData.get("HoudiniSavePath", None) if save_path: # Unfortunately this doesn't actually resolve the full absolute path return layer.ComputeAbsolutePath(save_path) def get_referenced_layers(layer): """Return SdfLayers for all external references of the current layer Args: layer (pxr.Sdf.Layer): The Layer to retrieve the save pah data from. Returns: list: List of pxr.Sdf.Layer that are external references to this layer """ layers = [] for layer_id in layer.GetExternalReferences(): layer = Sdf.Layer.Find(layer_id) if not layer: # A file may not be in memory and is # referenced from disk. As such it cannot # be found. We will ignore those layers. continue layers.append(layer) return layers def iter_layer_recursive(layer): """Recursively iterate all 'external' referenced layers""" layers = get_referenced_layers(layer) traversed = set(layers) # Avoid recursion to itself (if even possible) traverse = list(layers) for layer in traverse: # Include children layers (recursion) children_layers = get_referenced_layers(layer) children_layers = [x for x in children_layers if x not in traversed] traverse.extend(children_layers) traversed.update(children_layers) yield layer def get_configured_save_layers(usd_rop): lop_node = get_usd_rop_loppath(usd_rop) stage = lop_node.stage(apply_viewport_overrides=False) if not stage: raise RuntimeError( "No valid USD stage for ROP node: " "%s" % usd_rop.path() ) root_layer = stage.GetRootLayer() save_layers = [] for layer in iter_layer_recursive(root_layer): save_path = get_layer_save_path(layer) if save_path is not None: save_layers.append(layer) return save_layers ================================================ FILE: openpype/hosts/houdini/hooks/set_paths.py ================================================ from openpype.lib.applications import PreLaunchHook, LaunchTypes class SetPath(PreLaunchHook): """Set current dir to workdir. Hook `GlobalHostDataHook` must be executed before this hook. """ app_groups = {"houdini"} launch_types = {LaunchTypes.local} def execute(self): workdir = self.launch_context.env.get("AVALON_WORKDIR", "") if not workdir: self.log.warning("BUG: Workdir is not filled.") return self.launch_context.kwargs["cwd"] = workdir ================================================ FILE: openpype/hosts/houdini/plugins/create/convert_legacy.py ================================================ # -*- coding: utf-8 -*- """Converter for legacy Houdini subsets.""" from openpype.pipeline.create.creator_plugins import SubsetConvertorPlugin from openpype.hosts.houdini.api.lib import imprint class HoudiniLegacyConvertor(SubsetConvertorPlugin): """Find and convert any legacy subsets in the scene. This Converter will find all legacy subsets in the scene and will transform them to the current system. Since the old subsets doesn't retain any information about their original creators, the only mapping we can do is based on their families. Its limitation is that you can have multiple creators creating subset of the same family and there is no way to handle it. This code should nevertheless cover all creators that came with OpenPype. """ identifier = "io.openpype.creators.houdini.legacy" family_to_id = { "camera": "io.openpype.creators.houdini.camera", "ass": "io.openpype.creators.houdini.ass", "imagesequence": "io.openpype.creators.houdini.imagesequence", "hda": "io.openpype.creators.houdini.hda", "pointcache": "io.openpype.creators.houdini.pointcache", "redshiftproxy": "io.openpype.creators.houdini.redshiftproxy", "redshift_rop": "io.openpype.creators.houdini.redshift_rop", "usd": "io.openpype.creators.houdini.usd", "usdrender": "io.openpype.creators.houdini.usdrender", "vdbcache": "io.openpype.creators.houdini.vdbcache" } def __init__(self, *args, **kwargs): super(HoudiniLegacyConvertor, self).__init__(*args, **kwargs) self.legacy_subsets = {} def find_instances(self): """Find legacy subsets in the scene. Legacy subsets are the ones that doesn't have `creator_identifier` parameter on them. This is using cached entries done in :py:meth:`~HoudiniCreatorBase.cache_subsets()` """ self.legacy_subsets = self.collection_shared_data.get( "houdini_cached_legacy_subsets") if not self.legacy_subsets: return self.add_convertor_item("Found {} incompatible subset{}.".format( len(self.legacy_subsets), "s" if len(self.legacy_subsets) > 1 else "") ) def convert(self): """Convert all legacy subsets to current. It is enough to add `creator_identifier` and `instance_node`. """ if not self.legacy_subsets: return for family, subsets in self.legacy_subsets.items(): if family in self.family_to_id: for subset in subsets: data = { "creator_identifier": self.family_to_id[family], "instance_node": subset.path() } if family == "pointcache": data["families"] = ["abc"] self.log.info("Converting {} to {}".format( subset.path(), self.family_to_id[family])) imprint(subset, data) ================================================ FILE: openpype/hosts/houdini/plugins/create/create_alembic_camera.py ================================================ # -*- coding: utf-8 -*- """Creator plugin for creating alembic camera subsets.""" from openpype.hosts.houdini.api import plugin from openpype.pipeline import CreatedInstance, CreatorError import hou class CreateAlembicCamera(plugin.HoudiniCreator): """Single baked camera from Alembic ROP.""" identifier = "io.openpype.creators.houdini.camera" label = "Camera (Abc)" family = "camera" icon = "camera" def create(self, subset_name, instance_data, pre_create_data): import hou instance_data.pop("active", None) instance_data.update({"node_type": "alembic"}) instance = super(CreateAlembicCamera, self).create( subset_name, instance_data, pre_create_data) # type: CreatedInstance instance_node = hou.node(instance.get("instance_node")) parms = { "filename": hou.text.expandString( "$HIP/pyblish/{}.abc".format(subset_name)), "use_sop_path": False, } if self.selected_nodes: if len(self.selected_nodes) > 1: raise CreatorError("More than one item selected.") path = self.selected_nodes[0].path() # Split the node path into the first root and the remainder # So we can set the root and objects parameters correctly _, root, remainder = path.split("/", 2) parms.update({"root": "/" + root, "objects": remainder}) instance_node.setParms(parms) # Lock the Use Sop Path setting so the # user doesn't accidentally enable it. to_lock = ["use_sop_path"] self.lock_parameters(instance_node, to_lock) instance_node.parm("trange").set(1) def get_network_categories(self): return [ hou.ropNodeTypeCategory(), hou.objNodeTypeCategory() ] ================================================ FILE: openpype/hosts/houdini/plugins/create/create_arnold_ass.py ================================================ # -*- coding: utf-8 -*- """Creator plugin for creating Arnold ASS files.""" from openpype.hosts.houdini.api import plugin from openpype.lib import BoolDef class CreateArnoldAss(plugin.HoudiniCreator): """Arnold .ass Archive""" identifier = "io.openpype.creators.houdini.ass" label = "Arnold ASS" family = "ass" icon = "magic" # Default extension: `.ass` or `.ass.gz` # however calling HoudiniCreator.create() # will override it by the value in the project settings ext = ".ass" def create(self, subset_name, instance_data, pre_create_data): import hou instance_data.pop("active", None) instance_data.update({"node_type": "arnold"}) creator_attributes = instance_data.setdefault( "creator_attributes", dict()) creator_attributes["farm"] = pre_create_data["farm"] instance = super(CreateArnoldAss, self).create( subset_name, instance_data, pre_create_data) # type: plugin.CreatedInstance instance_node = hou.node(instance.get("instance_node")) # Hide Properties Tab on Arnold ROP since that's used # for rendering instead of .ass Archive Export parm_template_group = instance_node.parmTemplateGroup() parm_template_group.hideFolder("Properties", True) instance_node.setParmTemplateGroup(parm_template_group) filepath = "{}{}".format( hou.text.expandString("$HIP/pyblish/"), "{}.$F4{}".format(subset_name, self.ext) ) parms = { # Render frame range "trange": 1, # Arnold ROP settings "ar_ass_file": filepath, "ar_ass_export_enable": 1 } instance_node.setParms(parms) # Lock any parameters in this list to_lock = ["ar_ass_export_enable", "family", "id"] self.lock_parameters(instance_node, to_lock) def get_instance_attr_defs(self): return [ BoolDef("farm", label="Submitting to Farm", default=False) ] def get_pre_create_attr_defs(self): attrs = super().get_pre_create_attr_defs() # Use same attributes as for instance attributes return attrs + self.get_instance_attr_defs() ================================================ FILE: openpype/hosts/houdini/plugins/create/create_arnold_rop.py ================================================ from openpype.hosts.houdini.api import plugin from openpype.lib import EnumDef, BoolDef class CreateArnoldRop(plugin.HoudiniCreator): """Arnold ROP""" identifier = "io.openpype.creators.houdini.arnold_rop" label = "Arnold ROP" family = "arnold_rop" icon = "magic" # Default extension ext = "exr" # Default to split export and render jobs export_job = True def create(self, subset_name, instance_data, pre_create_data): import hou # Remove the active, we are checking the bypass flag of the nodes instance_data.pop("active", None) instance_data.update({"node_type": "arnold"}) # Add chunk size attribute instance_data["chunkSize"] = 1 # Submit for job publishing instance_data["farm"] = pre_create_data.get("farm") instance = super(CreateArnoldRop, self).create( subset_name, instance_data, pre_create_data) # type: plugin.CreatedInstance instance_node = hou.node(instance.get("instance_node")) ext = pre_create_data.get("image_format") filepath = "{renders_dir}{subset_name}/{subset_name}.$F4.{ext}".format( renders_dir=hou.text.expandString("$HIP/pyblish/renders/"), subset_name=subset_name, ext=ext, ) parms = { # Render frame range "trange": 1, # Arnold ROP settings "ar_picture": filepath, "ar_exr_half_precision": 1 # half precision } if pre_create_data.get("export_job"): ass_filepath = \ "{export_dir}{subset_name}/{subset_name}.$F4.ass".format( export_dir=hou.text.expandString("$HIP/pyblish/ass/"), subset_name=subset_name, ) parms["ar_ass_export_enable"] = 1 parms["ar_ass_file"] = ass_filepath instance_node.setParms(parms) # Lock any parameters in this list to_lock = ["family", "id"] self.lock_parameters(instance_node, to_lock) def get_pre_create_attr_defs(self): attrs = super(CreateArnoldRop, self).get_pre_create_attr_defs() image_format_enum = [ "bmp", "cin", "exr", "jpg", "pic", "pic.gz", "png", "rad", "rat", "rta", "sgi", "tga", "tif", ] return attrs + [ BoolDef("farm", label="Submitting to Farm", default=True), BoolDef("export_job", label="Split export and render jobs", default=self.export_job), EnumDef("image_format", image_format_enum, default=self.ext, label="Image Format Options") ] ================================================ FILE: openpype/hosts/houdini/plugins/create/create_bgeo.py ================================================ # -*- coding: utf-8 -*- """Creator plugin for creating pointcache bgeo files.""" from openpype.hosts.houdini.api import plugin from openpype.pipeline import CreatedInstance, CreatorError import hou from openpype.lib import EnumDef, BoolDef class CreateBGEO(plugin.HoudiniCreator): """BGEO pointcache creator.""" identifier = "io.openpype.creators.houdini.bgeo" label = "PointCache (Bgeo)" family = "pointcache" icon = "gears" def create(self, subset_name, instance_data, pre_create_data): instance_data.pop("active", None) instance_data.update({"node_type": "geometry"}) creator_attributes = instance_data.setdefault( "creator_attributes", dict()) creator_attributes["farm"] = pre_create_data["farm"] instance = super(CreateBGEO, self).create( subset_name, instance_data, pre_create_data) # type: CreatedInstance instance_node = hou.node(instance.get("instance_node")) file_path = "{}{}".format( hou.text.expandString("$HIP/pyblish/"), "{}.$F4.{}".format( subset_name, pre_create_data.get("bgeo_type") or "bgeo.sc") ) parms = { "sopoutput": file_path } instance_node.parm("trange").set(1) if self.selected_nodes: # if selection is on SOP level, use it if isinstance(self.selected_nodes[0], hou.SopNode): parms["soppath"] = self.selected_nodes[0].path() else: # try to find output node with the lowest index outputs = [ child for child in self.selected_nodes[0].children() if child.type().name() == "output" ] if not outputs: instance_node.setParms(parms) raise CreatorError(( "Missing output node in SOP level for the selection. " "Please select correct SOP path in created instance." )) outputs.sort(key=lambda output: output.evalParm("outputidx")) parms["soppath"] = outputs[0].path() instance_node.setParms(parms) def get_instance_attr_defs(self): return [ BoolDef("farm", label="Submitting to Farm", default=False) ] def get_pre_create_attr_defs(self): attrs = super().get_pre_create_attr_defs() bgeo_enum = [ { "value": "bgeo", "label": "uncompressed bgeo (.bgeo)" }, { "value": "bgeosc", "label": "BLOSC compressed bgeo (.bgeosc)" }, { "value": "bgeo.sc", "label": "BLOSC compressed bgeo (.bgeo.sc)" }, { "value": "bgeo.gz", "label": "GZ compressed bgeo (.bgeo.gz)" }, { "value": "bgeo.lzma", "label": "LZMA compressed bgeo (.bgeo.lzma)" }, { "value": "bgeo.bz2", "label": "BZip2 compressed bgeo (.bgeo.bz2)" } ] return attrs + [ EnumDef("bgeo_type", bgeo_enum, label="BGEO Options"), ] + self.get_instance_attr_defs() def get_network_categories(self): return [ hou.ropNodeTypeCategory(), hou.sopNodeTypeCategory() ] ================================================ FILE: openpype/hosts/houdini/plugins/create/create_composite.py ================================================ # -*- coding: utf-8 -*- """Creator plugin for creating composite sequences.""" from openpype.hosts.houdini.api import plugin from openpype.pipeline import CreatedInstance, CreatorError import hou class CreateCompositeSequence(plugin.HoudiniCreator): """Composite ROP to Image Sequence""" identifier = "io.openpype.creators.houdini.imagesequence" label = "Composite (Image Sequence)" family = "imagesequence" icon = "gears" ext = ".exr" def create(self, subset_name, instance_data, pre_create_data): import hou # noqa instance_data.pop("active", None) instance_data.update({"node_type": "comp"}) instance = super(CreateCompositeSequence, self).create( subset_name, instance_data, pre_create_data) # type: CreatedInstance instance_node = hou.node(instance.get("instance_node")) filepath = "{}{}".format( hou.text.expandString("$HIP/pyblish/"), "{}.$F4{}".format(subset_name, self.ext) ) parms = { "trange": 1, "copoutput": filepath } if self.selected_nodes: if len(self.selected_nodes) > 1: raise CreatorError("More than one item selected.") path = self.selected_nodes[0].path() parms["coppath"] = path instance_node.setParms(parms) # Manually set f1 & f2 to $FSTART and $FEND respectively # to match other Houdini nodes default. instance_node.parm("f1").setExpression("$FSTART") instance_node.parm("f2").setExpression("$FEND") # Lock any parameters in this list to_lock = ["prim_to_detail_pattern"] self.lock_parameters(instance_node, to_lock) def get_network_categories(self): return [ hou.ropNodeTypeCategory(), hou.cop2NodeTypeCategory() ] ================================================ FILE: openpype/hosts/houdini/plugins/create/create_hda.py ================================================ # -*- coding: utf-8 -*- """Creator plugin for creating publishable Houdini Digital Assets.""" from openpype.client import ( get_asset_by_name, get_subsets, ) from openpype.hosts.houdini.api import plugin import hou class CreateHDA(plugin.HoudiniCreator): """Publish Houdini Digital Asset file.""" identifier = "io.openpype.creators.houdini.hda" label = "Houdini Digital Asset (Hda)" family = "hda" icon = "gears" maintain_selection = False def _check_existing(self, asset_name, subset_name): # type: (str) -> bool """Check if existing subset name versions already exists.""" # Get all subsets of the current asset project_name = self.project_name asset_doc = get_asset_by_name( project_name, asset_name, fields=["_id"] ) subset_docs = get_subsets( project_name, asset_ids=[asset_doc["_id"]], fields=["name"] ) existing_subset_names_low = { subset_doc["name"].lower() for subset_doc in subset_docs } return subset_name.lower() in existing_subset_names_low def create_instance_node( self, asset_name, node_name, parent, node_type="geometry" ): parent_node = hou.node("/obj") if self.selected_nodes: # if we have `use selection` enabled, and we have some # selected nodes ... subnet = parent_node.collapseIntoSubnet( self.selected_nodes, subnet_name="{}_subnet".format(node_name)) subnet.moveToGoodPosition() to_hda = subnet else: to_hda = parent_node.createNode( "subnet", node_name="{}_subnet".format(node_name)) if not to_hda.type().definition(): # if node type has not its definition, it is not user # created hda. We test if hda can be created from the node. if not to_hda.canCreateDigitalAsset(): raise plugin.OpenPypeCreatorError( "cannot create hda from node {}".format(to_hda)) hda_node = to_hda.createDigitalAsset( name=node_name, hda_file_name="$HIP/{}.hda".format(node_name) ) hda_node.layoutChildren() elif self._check_existing(asset_name, node_name): raise plugin.OpenPypeCreatorError( ("subset {} is already published with different HDA" "definition.").format(node_name)) else: hda_node = to_hda hda_node.setName(node_name) self.customize_node_look(hda_node) return hda_node def create(self, subset_name, instance_data, pre_create_data): instance_data.pop("active", None) instance = super(CreateHDA, self).create( subset_name, instance_data, pre_create_data) # type: plugin.CreatedInstance return instance def get_network_categories(self): return [ hou.objNodeTypeCategory() ] ================================================ FILE: openpype/hosts/houdini/plugins/create/create_karma_rop.py ================================================ # -*- coding: utf-8 -*- """Creator plugin to create Karma ROP.""" from openpype.hosts.houdini.api import plugin from openpype.pipeline import CreatedInstance from openpype.lib import BoolDef, EnumDef, NumberDef class CreateKarmaROP(plugin.HoudiniCreator): """Karma ROP""" identifier = "io.openpype.creators.houdini.karma_rop" label = "Karma ROP" family = "karma_rop" icon = "magic" def create(self, subset_name, instance_data, pre_create_data): import hou # noqa instance_data.pop("active", None) instance_data.update({"node_type": "karma"}) # Add chunk size attribute instance_data["chunkSize"] = 10 # Submit for job publishing instance_data["farm"] = pre_create_data.get("farm") instance = super(CreateKarmaROP, self).create( subset_name, instance_data, pre_create_data) # type: CreatedInstance instance_node = hou.node(instance.get("instance_node")) ext = pre_create_data.get("image_format") filepath = "{renders_dir}{subset_name}/{subset_name}.$F4.{ext}".format( renders_dir=hou.text.expandString("$HIP/pyblish/renders/"), subset_name=subset_name, ext=ext, ) checkpoint = "{cp_dir}{subset_name}.$F4.checkpoint".format( cp_dir=hou.text.expandString("$HIP/pyblish/"), subset_name=subset_name ) usd_directory = "{usd_dir}{subset_name}_$RENDERID".format( usd_dir=hou.text.expandString("$HIP/pyblish/renders/usd_renders/"), # noqa subset_name=subset_name ) parms = { # Render Frame Range "trange": 1, # Karma ROP Setting "picture": filepath, # Karma Checkpoint Setting "productName": checkpoint, # USD Output Directory "savetodirectory": usd_directory, } res_x = pre_create_data.get("res_x") res_y = pre_create_data.get("res_y") if self.selected_nodes: # If camera found in selection # we will use as render camera camera = None for node in self.selected_nodes: if node.type().name() == "cam": camera = node.path() has_camera = pre_create_data.get("cam_res") if has_camera: res_x = node.evalParm("resx") res_y = node.evalParm("resy") if not camera: self.log.warning("No render camera found in selection") parms.update({ "camera": camera or "", "resolutionx": res_x, "resolutiony": res_y, }) instance_node.setParms(parms) # Lock some Avalon attributes to_lock = ["family", "id"] self.lock_parameters(instance_node, to_lock) def get_pre_create_attr_defs(self): attrs = super(CreateKarmaROP, self).get_pre_create_attr_defs() image_format_enum = [ "bmp", "cin", "exr", "jpg", "pic", "pic.gz", "png", "rad", "rat", "rta", "sgi", "tga", "tif", ] return attrs + [ BoolDef("farm", label="Submitting to Farm", default=True), EnumDef("image_format", image_format_enum, default="exr", label="Image Format Options"), NumberDef("res_x", label="width", default=1920, decimals=0), NumberDef("res_y", label="height", default=720, decimals=0), BoolDef("cam_res", label="Camera Resolution", default=False) ] ================================================ FILE: openpype/hosts/houdini/plugins/create/create_mantra_ifd.py ================================================ # -*- coding: utf-8 -*- """Creator plugin for creating pointcache alembics.""" from openpype.hosts.houdini.api import plugin from openpype.pipeline import CreatedInstance from openpype.lib import BoolDef class CreateMantraIFD(plugin.HoudiniCreator): """Mantra .ifd Archive""" identifier = "io.openpype.creators.houdini.mantraifd" label = "Mantra IFD" family = "mantraifd" icon = "gears" def create(self, subset_name, instance_data, pre_create_data): import hou instance_data.pop("active", None) instance_data.update({"node_type": "ifd"}) creator_attributes = instance_data.setdefault( "creator_attributes", dict()) creator_attributes["farm"] = pre_create_data["farm"] instance = super(CreateMantraIFD, self).create( subset_name, instance_data, pre_create_data) # type: CreatedInstance instance_node = hou.node(instance.get("instance_node")) filepath = "{}{}".format( hou.text.expandString("$HIP/pyblish/"), "{}.$F4.ifd".format(subset_name)) parms = { # Render frame range "trange": 1, # Arnold ROP settings "soho_diskfile": filepath, "soho_outputmode": 1 } instance_node.setParms(parms) # Lock any parameters in this list to_lock = ["soho_outputmode", "family", "id"] self.lock_parameters(instance_node, to_lock) def get_instance_attr_defs(self): return [ BoolDef("farm", label="Submitting to Farm", default=False) ] def get_pre_create_attr_defs(self): attrs = super().get_pre_create_attr_defs() # Use same attributes as for instance attributes return attrs + self.get_instance_attr_defs() ================================================ FILE: openpype/hosts/houdini/plugins/create/create_mantra_rop.py ================================================ # -*- coding: utf-8 -*- """Creator plugin to create Mantra ROP.""" from openpype.hosts.houdini.api import plugin from openpype.pipeline import CreatedInstance from openpype.lib import EnumDef, BoolDef class CreateMantraROP(plugin.HoudiniCreator): """Mantra ROP""" identifier = "io.openpype.creators.houdini.mantra_rop" label = "Mantra ROP" family = "mantra_rop" icon = "magic" # Default to split export and render jobs export_job = True def create(self, subset_name, instance_data, pre_create_data): import hou # noqa instance_data.pop("active", None) instance_data.update({"node_type": "ifd"}) # Add chunk size attribute instance_data["chunkSize"] = 10 # Submit for job publishing instance_data["farm"] = pre_create_data.get("farm") instance = super(CreateMantraROP, self).create( subset_name, instance_data, pre_create_data) # type: CreatedInstance instance_node = hou.node(instance.get("instance_node")) ext = pre_create_data.get("image_format") filepath = "{renders_dir}{subset_name}/{subset_name}.$F4.{ext}".format( renders_dir=hou.text.expandString("$HIP/pyblish/renders/"), subset_name=subset_name, ext=ext, ) parms = { # Render Frame Range "trange": 1, # Mantra ROP Setting "vm_picture": filepath, } if pre_create_data.get("export_job"): ifd_filepath = \ "{export_dir}{subset_name}/{subset_name}.$F4.ifd".format( export_dir=hou.text.expandString("$HIP/pyblish/ifd/"), subset_name=subset_name, ) parms["soho_outputmode"] = 1 parms["soho_diskfile"] = ifd_filepath if self.selected_nodes: # If camera found in selection # we will use as render camera camera = None for node in self.selected_nodes: if node.type().name() == "cam": camera = node.path() if not camera: self.log.warning("No render camera found in selection") parms.update({"camera": camera or ""}) custom_res = pre_create_data.get("override_resolution") if custom_res: parms.update({"override_camerares": 1}) instance_node.setParms(parms) # Lock some Avalon attributes to_lock = ["family", "id"] self.lock_parameters(instance_node, to_lock) def get_pre_create_attr_defs(self): attrs = super(CreateMantraROP, self).get_pre_create_attr_defs() image_format_enum = [ "bmp", "cin", "exr", "jpg", "pic", "pic.gz", "png", "rad", "rat", "rta", "sgi", "tga", "tif", ] return attrs + [ BoolDef("farm", label="Submitting to Farm", default=True), BoolDef("export_job", label="Split export and render jobs", default=self.export_job), EnumDef("image_format", image_format_enum, default="exr", label="Image Format Options"), BoolDef("override_resolution", label="Override Camera Resolution", tooltip="Override the current camera " "resolution, recommended for IPR.", default=False) ] ================================================ FILE: openpype/hosts/houdini/plugins/create/create_pointcache.py ================================================ # -*- coding: utf-8 -*- """Creator plugin for creating pointcache alembics.""" from openpype.hosts.houdini.api import plugin from openpype.lib import BoolDef import hou class CreatePointCache(plugin.HoudiniCreator): """Alembic ROP to pointcache""" identifier = "io.openpype.creators.houdini.pointcache" label = "PointCache (Abc)" family = "pointcache" icon = "gears" def create(self, subset_name, instance_data, pre_create_data): instance_data.pop("active", None) instance_data.update({"node_type": "alembic"}) creator_attributes = instance_data.setdefault( "creator_attributes", dict()) creator_attributes["farm"] = pre_create_data["farm"] instance = super(CreatePointCache, self).create( subset_name, instance_data, pre_create_data) instance_node = hou.node(instance.get("instance_node")) parms = { "use_sop_path": True, "build_from_path": True, "path_attrib": "path", "prim_to_detail_pattern": "cbId", "format": 2, "facesets": 0, "filename": hou.text.expandString( "$HIP/pyblish/{}.abc".format(subset_name)) } if self.selected_nodes: selected_node = self.selected_nodes[0] # Although Houdini allows ObjNode path on `sop_path` for the # the ROP node we prefer it set to the SopNode path explicitly # Allow sop level paths (e.g. /obj/geo1/box1) if isinstance(selected_node, hou.SopNode): parms["sop_path"] = selected_node.path() self.log.debug( "Valid SopNode selection, 'SOP Path' in ROP will be set to '%s'." % selected_node.path() ) # Allow object level paths to Geometry nodes (e.g. /obj/geo1) # but do not allow other object level nodes types like cameras, etc. elif isinstance(selected_node, hou.ObjNode) and \ selected_node.type().name() in ["geo"]: # get the output node with the minimum # 'outputidx' or the node with display flag sop_path = self.get_obj_output(selected_node) if sop_path: parms["sop_path"] = sop_path.path() self.log.debug( "Valid ObjNode selection, 'SOP Path' in ROP will be set to " "the child path '%s'." % sop_path.path() ) if not parms.get("sop_path", None): self.log.debug( "Selection isn't valid. 'SOP Path' in ROP will be empty." ) else: self.log.debug( "No Selection. 'SOP Path' in ROP will be empty." ) instance_node.setParms(parms) instance_node.parm("trange").set(1) # Lock any parameters in this list to_lock = ["prim_to_detail_pattern"] self.lock_parameters(instance_node, to_lock) def get_network_categories(self): return [ hou.ropNodeTypeCategory(), hou.sopNodeTypeCategory() ] def get_obj_output(self, obj_node): """Find output node with the smallest 'outputidx'.""" outputs = obj_node.subnetOutputs() # if obj_node is empty if not outputs: return # if obj_node has one output child whether its # sop output node or a node with the render flag elif len(outputs) == 1: return outputs[0] # if there are more than one, then it have multiple ouput nodes # return the one with the minimum 'outputidx' else: return min(outputs, key=lambda node: node.evalParm('outputidx')) def get_instance_attr_defs(self): return [ BoolDef("farm", label="Submitting to Farm", default=False) ] def get_pre_create_attr_defs(self): attrs = super().get_pre_create_attr_defs() # Use same attributes as for instance attributes return attrs + self.get_instance_attr_defs() ================================================ FILE: openpype/hosts/houdini/plugins/create/create_redshift_proxy.py ================================================ # -*- coding: utf-8 -*- """Creator plugin for creating Redshift proxies.""" from openpype.hosts.houdini.api import plugin import hou from openpype.lib import BoolDef class CreateRedshiftProxy(plugin.HoudiniCreator): """Redshift Proxy""" identifier = "io.openpype.creators.houdini.redshiftproxy" label = "Redshift Proxy" family = "redshiftproxy" icon = "magic" def create(self, subset_name, instance_data, pre_create_data): # Remove the active, we are checking the bypass flag of the nodes instance_data.pop("active", None) # Redshift provides a `Redshift_Proxy_Output` node type which shows # a limited set of parameters by default and is set to extract a # Redshift Proxy. However when "imprinting" extra parameters needed # for OpenPype it starts showing all its parameters again. It's unclear # why this happens. # TODO: Somehow enforce so that it only shows the original limited # attributes of the Redshift_Proxy_Output node type instance_data.update({"node_type": "Redshift_Proxy_Output"}) creator_attributes = instance_data.setdefault( "creator_attributes", dict()) creator_attributes["farm"] = pre_create_data["farm"] instance = super(CreateRedshiftProxy, self).create( subset_name, instance_data, pre_create_data) instance_node = hou.node(instance.get("instance_node")) parms = { "RS_archive_file": '$HIP/pyblish/{}.$F4.rs'.format(subset_name), } if self.selected_nodes: parms["RS_archive_sopPath"] = self.selected_nodes[0].path() instance_node.setParms(parms) # Lock some Avalon attributes to_lock = ["family", "id", "prim_to_detail_pattern"] self.lock_parameters(instance_node, to_lock) def get_network_categories(self): return [ hou.ropNodeTypeCategory(), hou.sopNodeTypeCategory() ] def get_instance_attr_defs(self): return [ BoolDef("farm", label="Submitting to Farm", default=False) ] def get_pre_create_attr_defs(self): attrs = super().get_pre_create_attr_defs() # Use same attributes as for instance attributes return attrs + self.get_instance_attr_defs() ================================================ FILE: openpype/hosts/houdini/plugins/create/create_redshift_rop.py ================================================ # -*- coding: utf-8 -*- """Creator plugin to create Redshift ROP.""" import hou # noqa from openpype.hosts.houdini.api import plugin from openpype.lib import EnumDef, BoolDef class CreateRedshiftROP(plugin.HoudiniCreator): """Redshift ROP""" identifier = "io.openpype.creators.houdini.redshift_rop" label = "Redshift ROP" family = "redshift_rop" icon = "magic" ext = "exr" # Default to split export and render jobs split_render = True def create(self, subset_name, instance_data, pre_create_data): instance_data.pop("active", None) instance_data.update({"node_type": "Redshift_ROP"}) # Add chunk size attribute instance_data["chunkSize"] = 10 # Submit for job publishing instance_data["farm"] = pre_create_data.get("farm") instance = super(CreateRedshiftROP, self).create( subset_name, instance_data, pre_create_data) instance_node = hou.node(instance.get("instance_node")) basename = instance_node.name() # Also create the linked Redshift IPR Rop try: ipr_rop = instance_node.parent().createNode( "Redshift_IPR", node_name=f"{basename}_IPR" ) except hou.OperationFailed as e: raise plugin.OpenPypeCreatorError( ( "Cannot create Redshift node. Is Redshift " "installed and enabled?" ) ) from e # Move it to directly under the Redshift ROP ipr_rop.setPosition(instance_node.position() + hou.Vector2(0, -1)) # Set the linked rop to the Redshift ROP ipr_rop.parm("linked_rop").set(instance_node.path()) ext = pre_create_data.get("image_format") filepath = "{renders_dir}{subset_name}/{subset_name}.{fmt}".format( renders_dir=hou.text.expandString("$HIP/pyblish/renders/"), subset_name=subset_name, fmt="${aov}.$F4.{ext}".format(aov="AOV", ext=ext) ) ext_format_index = {"exr": 0, "tif": 1, "jpg": 2, "png": 3} parms = { # Render frame range "trange": 1, # Redshift ROP settings "RS_outputFileNamePrefix": filepath, "RS_outputMultilayerMode": "1", # no multi-layered exr "RS_outputBeautyAOVSuffix": "beauty", "RS_outputFileFormat": ext_format_index[ext], } if self.selected_nodes: # set up the render camera from the selected node camera = None for node in self.selected_nodes: if node.type().name() == "cam": camera = node.path() parms["RS_renderCamera"] = camera or "" export_dir = hou.text.expandString("$HIP/pyblish/rs/") rs_filepath = f"{export_dir}{subset_name}/{subset_name}.$F4.rs" parms["RS_archive_file"] = rs_filepath if pre_create_data.get("split_render", self.split_render): parms["RS_archive_enable"] = 1 instance_node.setParms(parms) # Lock some Avalon attributes to_lock = ["family", "id"] self.lock_parameters(instance_node, to_lock) def remove_instances(self, instances): for instance in instances: node = instance.data.get("instance_node") ipr_node = hou.node(f"{node}_IPR") if ipr_node: ipr_node.destroy() return super(CreateRedshiftROP, self).remove_instances(instances) def get_pre_create_attr_defs(self): attrs = super(CreateRedshiftROP, self).get_pre_create_attr_defs() image_format_enum = [ "exr", "tif", "jpg", "png", ] return attrs + [ BoolDef("farm", label="Submitting to Farm", default=True), BoolDef("split_render", label="Split export and render jobs", default=self.split_render), EnumDef("image_format", image_format_enum, default=self.ext, label="Image Format Options") ] ================================================ FILE: openpype/hosts/houdini/plugins/create/create_review.py ================================================ # -*- coding: utf-8 -*- """Creator plugin for creating openGL reviews.""" from openpype.hosts.houdini.api import plugin from openpype.lib import EnumDef, BoolDef, NumberDef import os import hou class CreateReview(plugin.HoudiniCreator): """Review with OpenGL ROP""" identifier = "io.openpype.creators.houdini.review" label = "Review" family = "review" icon = "video-camera" def create(self, subset_name, instance_data, pre_create_data): instance_data.pop("active", None) instance_data.update({"node_type": "opengl"}) instance_data["imageFormat"] = pre_create_data.get("imageFormat") instance_data["keepImages"] = pre_create_data.get("keepImages") instance = super(CreateReview, self).create( subset_name, instance_data, pre_create_data) instance_node = hou.node(instance.get("instance_node")) frame_range = hou.playbar.frameRange() filepath = "{root}/{subset}/{subset}.$F4.{ext}".format( root=hou.text.expandString("$HIP/pyblish"), subset="`chs(\"subset\")`", # keep dynamic link to subset ext=pre_create_data.get("image_format") or "png" ) parms = { "picture": filepath, "trange": 1, # Unlike many other ROP nodes the opengl node does not default # to expression of $FSTART and $FEND so we preserve that behavior # but do set the range to the frame range of the playbar "f1": frame_range[0], "f2": frame_range[1], } override_resolution = pre_create_data.get("override_resolution") if override_resolution: parms.update({ "tres": override_resolution, "res1": pre_create_data.get("resx"), "res2": pre_create_data.get("resy"), "aspect": pre_create_data.get("aspect"), }) if self.selected_nodes: # The first camera found in selection we will use as camera # Other node types we set in force objects camera = None force_objects = [] for node in self.selected_nodes: path = node.path() if node.type().name() == "cam": if camera: continue camera = path else: force_objects.append(path) if not camera: self.log.warning("No camera found in selection.") parms.update({ "camera": camera or "", "scenepath": "/obj", "forceobjects": " ".join(force_objects), "vobjects": "" # clear candidate objects from '*' value }) instance_node.setParms(parms) # Set OCIO Colorspace to the default output colorspace # if there's OCIO if os.getenv("OCIO"): self.set_colorcorrect_to_default_view_space(instance_node) to_lock = ["id", "family"] self.lock_parameters(instance_node, to_lock) def get_pre_create_attr_defs(self): attrs = super(CreateReview, self).get_pre_create_attr_defs() image_format_enum = [ "bmp", "cin", "exr", "jpg", "pic", "pic.gz", "png", "rad", "rat", "rta", "sgi", "tga", "tif", ] return attrs + [ BoolDef("keepImages", label="Keep Image Sequences", default=False), EnumDef("imageFormat", image_format_enum, default="png", label="Image Format Options"), BoolDef("override_resolution", label="Override resolution", tooltip="When disabled the resolution set on the camera " "is used instead.", default=True), NumberDef("resx", label="Resolution Width", default=1280, minimum=2, decimals=0), NumberDef("resy", label="Resolution Height", default=720, minimum=2, decimals=0), NumberDef("aspect", label="Aspect Ratio", default=1.0, minimum=0.0001, decimals=3) ] def set_colorcorrect_to_default_view_space(self, instance_node): """Set ociocolorspace to the default output space.""" from openpype.hosts.houdini.api.colorspace import get_default_display_view_colorspace # noqa # set Color Correction parameter to OpenColorIO instance_node.setParms({"colorcorrect": 2}) # Get default view space for ociocolorspace parm. default_view_space = get_default_display_view_colorspace() instance_node.setParms( {"ociocolorspace": default_view_space} ) self.log.debug( "'OCIO Colorspace' parm on '{}' has been set to " "the default view color space '{}'" .format(instance_node, default_view_space) ) ================================================ FILE: openpype/hosts/houdini/plugins/create/create_staticmesh.py ================================================ # -*- coding: utf-8 -*- """Creator for Unreal Static Meshes.""" from openpype.hosts.houdini.api import plugin from openpype.lib import BoolDef, EnumDef import hou class CreateStaticMesh(plugin.HoudiniCreator): """Static Meshes as FBX. """ identifier = "io.openpype.creators.houdini.staticmesh.fbx" label = "Static Mesh (FBX)" family = "staticMesh" icon = "fa5s.cubes" default_variants = ["Main"] def create(self, subset_name, instance_data, pre_create_data): instance_data.update({"node_type": "filmboxfbx"}) instance = super(CreateStaticMesh, self).create( subset_name, instance_data, pre_create_data) # get the created rop node instance_node = hou.node(instance.get("instance_node")) # prepare parms output_path = hou.text.expandString( "$HIP/pyblish/{}.fbx".format(subset_name) ) parms = { "startnode": self.get_selection(), "sopoutput": output_path, # vertex cache format "vcformat": pre_create_data.get("vcformat"), "convertunits": pre_create_data.get("convertunits"), # set render range to use frame range start-end frame "trange": 1, "createsubnetroot": pre_create_data.get("createsubnetroot") } # set parms instance_node.setParms(parms) # Lock any parameters in this list to_lock = ["family", "id"] self.lock_parameters(instance_node, to_lock) def get_network_categories(self): return [ hou.ropNodeTypeCategory(), hou.objNodeTypeCategory(), hou.sopNodeTypeCategory() ] def get_pre_create_attr_defs(self): """Add settings for users. """ attrs = super(CreateStaticMesh, self).get_pre_create_attr_defs() createsubnetroot = BoolDef("createsubnetroot", tooltip="Create an extra root for the " "Export node when it's a " "subnetwork. This causes the " "exporting subnetwork node to be " "represented in the FBX file.", default=False, label="Create Root for Subnet") vcformat = EnumDef("vcformat", items={ 0: "Maya Compatible (MC)", 1: "3DS MAX Compatible (PC2)" }, default=0, label="Vertex Cache Format") convert_units = BoolDef("convertunits", tooltip="When on, the FBX is converted" "from the current Houdini " "system units to the native " "FBX unit of centimeters.", default=False, label="Convert Units") return attrs + [createsubnetroot, vcformat, convert_units] def get_dynamic_data( self, variant, task_name, asset_doc, project_name, host_name, instance ): """ The default subset name templates for Unreal include {asset} and thus we should pass that along as dynamic data. """ dynamic_data = super(CreateStaticMesh, self).get_dynamic_data( variant, task_name, asset_doc, project_name, host_name, instance ) dynamic_data["asset"] = asset_doc["name"] return dynamic_data def get_selection(self): """Selection Logic. how self.selected_nodes should be processed to get the desirable node from selection. Returns: str : node path """ selection = "" if self.selected_nodes: selected_node = self.selected_nodes[0] # Accept sop level nodes (e.g. /obj/geo1/box1) if isinstance(selected_node, hou.SopNode): selection = selected_node.path() self.log.debug( "Valid SopNode selection, 'Export' in filmboxfbx" " will be set to '%s'.", selected_node ) # Accept object level nodes (e.g. /obj/geo1) elif isinstance(selected_node, hou.ObjNode): selection = selected_node.path() self.log.debug( "Valid ObjNode selection, 'Export' in filmboxfbx " "will be set to the child path '%s'.", selection ) else: self.log.debug( "Selection isn't valid. 'Export' in " "filmboxfbx will be empty." ) else: self.log.debug( "No Selection. 'Export' in filmboxfbx will be empty." ) return selection ================================================ FILE: openpype/hosts/houdini/plugins/create/create_usd.py ================================================ # -*- coding: utf-8 -*- """Creator plugin for creating USDs.""" from openpype.hosts.houdini.api import plugin from openpype.pipeline import CreatedInstance import hou class CreateUSD(plugin.HoudiniCreator): """Universal Scene Description""" identifier = "io.openpype.creators.houdini.usd" label = "USD (experimental)" family = "usd" icon = "gears" enabled = False def create(self, subset_name, instance_data, pre_create_data): instance_data.pop("active", None) instance_data.update({"node_type": "usd"}) instance = super(CreateUSD, self).create( subset_name, instance_data, pre_create_data) # type: CreatedInstance instance_node = hou.node(instance.get("instance_node")) parms = { "lopoutput": "$HIP/pyblish/{}.usd".format(subset_name), "enableoutputprocessor_simplerelativepaths": False, } if self.selected_nodes: parms["loppath"] = self.selected_nodes[0].path() instance_node.setParms(parms) # Lock any parameters in this list to_lock = [ "fileperframe", # Lock some Avalon attributes "family", "id", ] self.lock_parameters(instance_node, to_lock) def get_network_categories(self): return [ hou.ropNodeTypeCategory(), hou.lopNodeTypeCategory() ] ================================================ FILE: openpype/hosts/houdini/plugins/create/create_usdrender.py ================================================ # -*- coding: utf-8 -*- """Creator plugin for creating USD renders.""" from openpype.hosts.houdini.api import plugin from openpype.pipeline import CreatedInstance class CreateUSDRender(plugin.HoudiniCreator): """USD Render ROP in /stage""" identifier = "io.openpype.creators.houdini.usdrender" label = "USD Render (experimental)" family = "usdrender" icon = "magic" def create(self, subset_name, instance_data, pre_create_data): import hou # noqa instance_data["parent"] = hou.node("/stage") # Remove the active, we are checking the bypass flag of the nodes instance_data.pop("active", None) instance_data.update({"node_type": "usdrender"}) instance = super(CreateUSDRender, self).create( subset_name, instance_data, pre_create_data) # type: CreatedInstance instance_node = hou.node(instance.get("instance_node")) parms = { # Render frame range "trange": 1 } if self.selected_nodes: parms["loppath"] = self.selected_nodes[0].path() instance_node.setParms(parms) # Lock some Avalon attributes to_lock = ["family", "id"] self.lock_parameters(instance_node, to_lock) ================================================ FILE: openpype/hosts/houdini/plugins/create/create_vbd_cache.py ================================================ # -*- coding: utf-8 -*- """Creator plugin for creating VDB Caches.""" from openpype.hosts.houdini.api import plugin from openpype.pipeline import CreatedInstance from openpype.lib import BoolDef import hou class CreateVDBCache(plugin.HoudiniCreator): """OpenVDB from Geometry ROP""" identifier = "io.openpype.creators.houdini.vdbcache" name = "vbdcache" label = "VDB Cache" family = "vdbcache" icon = "cloud" def create(self, subset_name, instance_data, pre_create_data): import hou instance_data.pop("active", None) instance_data.update({"node_type": "geometry"}) creator_attributes = instance_data.setdefault( "creator_attributes", dict()) creator_attributes["farm"] = pre_create_data["farm"] instance = super(CreateVDBCache, self).create( subset_name, instance_data, pre_create_data) # type: CreatedInstance instance_node = hou.node(instance.get("instance_node")) file_path = "{}{}".format( hou.text.expandString("$HIP/pyblish/"), "{}.$F4.vdb".format(subset_name)) parms = { "sopoutput": file_path, "initsim": True, "trange": 1 } if self.selected_nodes: parms["soppath"] = self.get_sop_node_path(self.selected_nodes[0]) instance_node.setParms(parms) def get_network_categories(self): return [ hou.ropNodeTypeCategory(), hou.objNodeTypeCategory(), hou.sopNodeTypeCategory() ] def get_sop_node_path(self, selected_node): """Get Sop Path of the selected node. Although Houdini allows ObjNode path on `sop_path` for the the ROP node, we prefer it set to the SopNode path explicitly. """ # Allow sop level paths (e.g. /obj/geo1/box1) if isinstance(selected_node, hou.SopNode): self.log.debug( "Valid SopNode selection, 'SOP Path' in ROP will" " be set to '%s'.", selected_node.path() ) return selected_node.path() # Allow object level paths to Geometry nodes (e.g. /obj/geo1) # but do not allow other object level nodes types like cameras, etc. elif isinstance(selected_node, hou.ObjNode) and \ selected_node.type().name() == "geo": # Try to find output node. sop_node = self.get_obj_output(selected_node) if sop_node: self.log.debug( "Valid ObjNode selection, 'SOP Path' in ROP will " "be set to the child path '%s'.", sop_node.path() ) return sop_node.path() self.log.debug( "Selection isn't valid. 'SOP Path' in ROP will be empty." ) return "" def get_obj_output(self, obj_node): """Try to find output node. If any output nodes are present, return the output node with the minimum 'outputidx' If no output nodes are present, return the node with display flag If no nodes are present at all, return None """ outputs = obj_node.subnetOutputs() # if obj_node is empty if not outputs: return # if obj_node has one output child whether its # sop output node or a node with the render flag elif len(outputs) == 1: return outputs[0] # if there are more than one, then it has multiple output nodes # return the one with the minimum 'outputidx' else: return min(outputs, key=lambda node: node.evalParm('outputidx')) def get_instance_attr_defs(self): return [ BoolDef("farm", label="Submitting to Farm", default=False) ] def get_pre_create_attr_defs(self): attrs = super().get_pre_create_attr_defs() # Use same attributes as for instance attributes return attrs + self.get_instance_attr_defs() ================================================ FILE: openpype/hosts/houdini/plugins/create/create_vray_rop.py ================================================ # -*- coding: utf-8 -*- """Creator plugin to create VRay ROP.""" import hou from openpype.hosts.houdini.api import plugin from openpype.pipeline import CreatedInstance from openpype.lib import EnumDef, BoolDef class CreateVrayROP(plugin.HoudiniCreator): """VRay ROP""" identifier = "io.openpype.creators.houdini.vray_rop" label = "VRay ROP" family = "vray_rop" icon = "magic" ext = "exr" # Default to split export and render jobs export_job = True def create(self, subset_name, instance_data, pre_create_data): instance_data.pop("active", None) instance_data.update({"node_type": "vray_renderer"}) # Add chunk size attribute instance_data["chunkSize"] = 10 # Submit for job publishing instance_data["farm"] = pre_create_data.get("farm") instance = super(CreateVrayROP, self).create( subset_name, instance_data, pre_create_data) # type: CreatedInstance instance_node = hou.node(instance.get("instance_node")) # Add IPR for Vray basename = instance_node.name() try: ipr_rop = instance_node.parent().createNode( "vray", node_name=basename + "_IPR" ) except hou.OperationFailed: raise plugin.OpenPypeCreatorError( "Cannot create Vray render node. " "Make sure Vray installed and enabled!" ) ipr_rop.setPosition(instance_node.position() + hou.Vector2(0, -1)) ipr_rop.parm("rop").set(instance_node.path()) parms = { "trange": 1, "SettingsEXR_bits_per_channel": "16" # half precision } if pre_create_data.get("export_job"): scene_filepath = \ "{export_dir}{subset_name}/{subset_name}.$F4.vrscene".format( export_dir=hou.text.expandString("$HIP/pyblish/vrscene/"), subset_name=subset_name, ) # Setting render_export_mode to "2" because that's for # "Export only" ("1" is for "Export & Render") parms["render_export_mode"] = "2" parms["render_export_filepath"] = scene_filepath if self.selected_nodes: # set up the render camera from the selected node camera = None for node in self.selected_nodes: if node.type().name() == "cam": camera = node.path() parms.update({ "render_camera": camera or "" }) # Enable render element ext = pre_create_data.get("image_format") instance_data["RenderElement"] = pre_create_data.get("render_element_enabled") # noqa if pre_create_data.get("render_element_enabled", True): # Vray has its own tag for AOV file output filepath = "{renders_dir}{subset_name}/{subset_name}.{fmt}".format( renders_dir=hou.text.expandString("$HIP/pyblish/renders/"), subset_name=subset_name, fmt="${aov}.$F4.{ext}".format(aov="AOV", ext=ext) ) filepath = "{}{}".format( hou.text.expandString("$HIP/pyblish/renders/"), "{}/{}.${}.$F4.{}".format(subset_name, subset_name, "AOV", ext) ) re_rop = instance_node.parent().createNode( "vray_render_channels", node_name=basename + "_render_element" ) # move the render element node next to the vray renderer node re_rop.setPosition(instance_node.position() + hou.Vector2(0, 1)) re_path = re_rop.path() parms.update({ "use_render_channels": 1, "SettingsOutput_img_file_path": filepath, "render_network_render_channels": re_path }) else: filepath = "{renders_dir}{subset_name}/{subset_name}.{fmt}".format( renders_dir=hou.text.expandString("$HIP/pyblish/renders/"), subset_name=subset_name, fmt="$F4.{ext}".format(ext=ext) ) parms.update({ "use_render_channels": 0, "SettingsOutput_img_file_path": filepath }) custom_res = pre_create_data.get("override_resolution") if custom_res: parms.update({"override_camerares": 1}) instance_node.setParms(parms) # lock parameters from AVALON to_lock = ["family", "id"] self.lock_parameters(instance_node, to_lock) def remove_instances(self, instances): for instance in instances: node = instance.data.get("instance_node") # for the extra render node from the plugins # such as vray and redshift ipr_node = hou.node("{}{}".format(node, "_IPR")) if ipr_node: ipr_node.destroy() re_node = hou.node("{}{}".format(node, "_render_element")) if re_node: re_node.destroy() return super(CreateVrayROP, self).remove_instances(instances) def get_pre_create_attr_defs(self): attrs = super(CreateVrayROP, self).get_pre_create_attr_defs() image_format_enum = [ "bmp", "cin", "exr", "jpg", "pic", "pic.gz", "png", "rad", "rat", "rta", "sgi", "tga", "tif", ] return attrs + [ BoolDef("farm", label="Submitting to Farm", default=True), BoolDef("export_job", label="Split export and render jobs", default=self.export_job), EnumDef("image_format", image_format_enum, default=self.ext, label="Image Format Options"), BoolDef("override_resolution", label="Override Camera Resolution", tooltip="Override the current camera " "resolution, recommended for IPR.", default=False), BoolDef("render_element_enabled", label="Render Element", tooltip="Create Render Element Node " "if enabled", default=False) ] ================================================ FILE: openpype/hosts/houdini/plugins/create/create_workfile.py ================================================ # -*- coding: utf-8 -*- """Creator plugin for creating workfiles.""" from openpype import AYON_SERVER_ENABLED from openpype.hosts.houdini.api import plugin from openpype.hosts.houdini.api.lib import read, imprint from openpype.hosts.houdini.api.pipeline import CONTEXT_CONTAINER from openpype.pipeline import CreatedInstance, AutoCreator from openpype.client import get_asset_by_name import hou class CreateWorkfile(plugin.HoudiniCreatorBase, AutoCreator): """Workfile auto-creator.""" identifier = "io.openpype.creators.houdini.workfile" label = "Workfile" family = "workfile" icon = "fa5.file" default_variant = "Main" def create(self): variant = self.default_variant current_instance = next( ( instance for instance in self.create_context.instances if instance.creator_identifier == self.identifier ), None) project_name = self.project_name asset_name = self.create_context.get_current_asset_name() task_name = self.create_context.get_current_task_name() host_name = self.host_name if current_instance is None: current_instance_asset = None elif AYON_SERVER_ENABLED: current_instance_asset = current_instance["folderPath"] else: current_instance_asset = current_instance["asset"] if current_instance is None: asset_doc = get_asset_by_name(project_name, asset_name) subset_name = self.get_subset_name( variant, task_name, asset_doc, project_name, host_name ) data = { "task": task_name, "variant": variant } if AYON_SERVER_ENABLED: data["folderPath"] = asset_name else: data["asset"] = asset_name data.update( self.get_dynamic_data( variant, task_name, asset_doc, project_name, host_name, current_instance) ) self.log.info("Auto-creating workfile instance...") current_instance = CreatedInstance( self.family, subset_name, data, self ) self._add_instance_to_context(current_instance) elif ( current_instance_asset != asset_name or current_instance["task"] != task_name ): # Update instance context if is not the same asset_doc = get_asset_by_name(project_name, asset_name) subset_name = self.get_subset_name( variant, task_name, asset_doc, project_name, host_name ) if AYON_SERVER_ENABLED: current_instance["folderPath"] = asset_name else: current_instance["asset"] = asset_name current_instance["task"] = task_name current_instance["subset"] = subset_name # write workfile information to context container. op_ctx = hou.node(CONTEXT_CONTAINER) if not op_ctx: op_ctx = self.create_context_node() workfile_data = {"workfile": current_instance.data_to_store()} imprint(op_ctx, workfile_data) def collect_instances(self): op_ctx = hou.node(CONTEXT_CONTAINER) instance = read(op_ctx) if not instance: return workfile = instance.get("workfile") if not workfile: return created_instance = CreatedInstance.from_existing( workfile, self ) self._add_instance_to_context(created_instance) def update_instances(self, update_list): op_ctx = hou.node(CONTEXT_CONTAINER) for created_inst, _changes in update_list: if created_inst["creator_identifier"] == self.identifier: workfile_data = {"workfile": created_inst.data_to_store()} imprint(op_ctx, workfile_data, update=True) ================================================ FILE: openpype/hosts/houdini/plugins/inventory/set_camera_resolution.py ================================================ from openpype.pipeline import InventoryAction from openpype.hosts.houdini.api.lib import ( get_camera_from_container, set_camera_resolution ) from openpype.pipeline.context_tools import get_current_project_asset class SetCameraResolution(InventoryAction): label = "Set Camera Resolution" icon = "desktop" color = "orange" @staticmethod def is_compatible(container): return ( container.get("loader") == "CameraLoader" ) def process(self, containers): asset_doc = get_current_project_asset() for container in containers: node = container["node"] camera = get_camera_from_container(node) set_camera_resolution(camera, asset_doc) ================================================ FILE: openpype/hosts/houdini/plugins/load/actions.py ================================================ """A module containing generic loader actions that will display in the Loader. """ from openpype.pipeline import load class SetFrameRangeLoader(load.LoaderPlugin): """Set frame range excluding pre- and post-handles""" families = [ "animation", "camera", "pointcache", "vdbcache", "usd", ] representations = ["abc", "vdb", "usd"] label = "Set frame range" order = 11 icon = "clock-o" color = "white" def load(self, context, name, namespace, data): import hou version = context["version"] version_data = version.get("data", {}) start = version_data.get("frameStart", None) end = version_data.get("frameEnd", None) if start is None or end is None: print( "Skipping setting frame range because start or " "end frame data is missing.." ) return hou.playbar.setFrameRange(start, end) hou.playbar.setPlaybackRange(start, end) class SetFrameRangeWithHandlesLoader(load.LoaderPlugin): """Set frame range including pre- and post-handles""" families = [ "animation", "camera", "pointcache", "vdbcache", "usd", ] representations = ["abc", "vdb", "usd"] label = "Set frame range (with handles)" order = 12 icon = "clock-o" color = "white" def load(self, context, name, namespace, data): import hou version = context["version"] version_data = version.get("data", {}) start = version_data.get("frameStart", None) end = version_data.get("frameEnd", None) if start is None or end is None: print( "Skipping setting frame range because start or " "end frame data is missing.." ) return # Include handles start -= version_data.get("handleStart", 0) end += version_data.get("handleEnd", 0) hou.playbar.setFrameRange(start, end) hou.playbar.setPlaybackRange(start, end) ================================================ FILE: openpype/hosts/houdini/plugins/load/load_alembic.py ================================================ import os from openpype.pipeline import ( load, get_representation_path, ) from openpype.hosts.houdini.api import pipeline class AbcLoader(load.LoaderPlugin): """Load Alembic""" families = ["model", "animation", "pointcache", "gpuCache"] label = "Load Alembic" representations = ["abc"] order = -10 icon = "code-fork" color = "orange" def load(self, context, name=None, namespace=None, data=None): import hou # Format file name, Houdini only wants forward slashes file_path = self.filepath_from_context(context) file_path = os.path.normpath(file_path) file_path = file_path.replace("\\", "/") # Get the root node obj = hou.node("/obj") # Define node name namespace = namespace if namespace else context["asset"]["name"] node_name = "{}_{}".format(namespace, name) if namespace else name # Create a new geo node container = obj.createNode("geo", node_name=node_name) # Remove the file node, it only loads static meshes # Houdini 17 has removed the file node from the geo node file_node = container.node("file1") if file_node: file_node.destroy() # Create an alembic node (supports animation) alembic = container.createNode("alembic", node_name=node_name) alembic.setParms({"fileName": file_path}) # Add unpack node unpack_name = "unpack_{}".format(name) unpack = container.createNode("unpack", node_name=unpack_name) unpack.setInput(0, alembic) unpack.setParms({"transfer_attributes": "path"}) # Add normal to points # Order of menu ['point', 'vertex', 'prim', 'detail'] normal_name = "normal_{}".format(name) normal_node = container.createNode("normal", node_name=normal_name) normal_node.setParms({"type": 0}) normal_node.setInput(0, unpack) null = container.createNode("null", node_name="OUT".format(name)) null.setInput(0, normal_node) # Ensure display flag is on the Alembic input node and not on the OUT # node to optimize "debug" displaying in the viewport. alembic.setDisplayFlag(True) # Set new position for unpack node else it gets cluttered nodes = [container, alembic, unpack, normal_node, null] for nr, node in enumerate(nodes): node.setPosition([0, (0 - nr)]) self[:] = nodes return pipeline.containerise( node_name, namespace, nodes, context, self.__class__.__name__, suffix="", ) def update(self, container, representation): node = container["node"] try: alembic_node = next( n for n in node.children() if n.type().name() == "alembic" ) except StopIteration: self.log.error("Could not find node of type `alembic`") return # Update the file path file_path = get_representation_path(representation) file_path = file_path.replace("\\", "/") alembic_node.setParms({"fileName": file_path}) # Update attribute node.setParms({"representation": str(representation["_id"])}) def remove(self, container): node = container["node"] node.destroy() def switch(self, container, representation): self.update(container, representation) ================================================ FILE: openpype/hosts/houdini/plugins/load/load_alembic_archive.py ================================================ import os from openpype.pipeline import ( load, get_representation_path, ) from openpype.hosts.houdini.api import pipeline class AbcArchiveLoader(load.LoaderPlugin): """Load Alembic as full geometry network hierarchy """ families = ["model", "animation", "pointcache", "gpuCache"] label = "Load Alembic as Archive" representations = ["abc"] order = -5 icon = "code-fork" color = "orange" def load(self, context, name=None, namespace=None, data=None): import hou # Format file name, Houdini only wants forward slashes file_path = self.filepath_from_context(context) file_path = os.path.normpath(file_path) file_path = file_path.replace("\\", "/") # Get the root node obj = hou.node("/obj") # Define node name namespace = namespace if namespace else context["asset"]["name"] node_name = "{}_{}".format(namespace, name) if namespace else name # Create an Alembic archive node node = obj.createNode("alembicarchive", node_name=node_name) node.moveToGoodPosition() # TODO: add FPS of project / asset node.setParms({"fileName": file_path, "channelRef": True}) # Apply some magic node.parm("buildHierarchy").pressButton() node.moveToGoodPosition() nodes = [node] self[:] = nodes return pipeline.containerise(node_name, namespace, nodes, context, self.__class__.__name__, suffix="") def update(self, container, representation): node = container["node"] # Update the file path file_path = get_representation_path(representation) file_path = file_path.replace("\\", "/") # Update attributes node.setParms({"fileName": file_path, "representation": str(representation["_id"])}) # Rebuild node.parm("buildHierarchy").pressButton() def remove(self, container): node = container["node"] node.destroy() def switch(self, container, representation): self.update(container, representation) ================================================ FILE: openpype/hosts/houdini/plugins/load/load_ass.py ================================================ import os import re from openpype.pipeline import ( load, get_representation_path, ) from openpype.hosts.houdini.api import pipeline class AssLoader(load.LoaderPlugin): """Load .ass with Arnold Procedural""" families = ["ass"] label = "Load Arnold Procedural" representations = ["ass"] order = -10 icon = "code-fork" color = "orange" def load(self, context, name=None, namespace=None, data=None): import hou # Get the root node obj = hou.node("/obj") # Define node name namespace = namespace if namespace else context["asset"]["name"] node_name = "{}_{}".format(namespace, name) if namespace else name # Create a new geo node procedural = obj.createNode("arnold::procedural", node_name=node_name) procedural.setParms( { "ar_filename": self.format_path(context["representation"]) }) nodes = [procedural] self[:] = nodes return pipeline.containerise( node_name, namespace, nodes, context, self.__class__.__name__, suffix="", ) def update(self, container, representation): # Update the file path procedural = container["node"] procedural.setParms({"ar_filename": self.format_path(representation)}) # Update attribute procedural.setParms({"representation": str(representation["_id"])}) def remove(self, container): node = container["node"] node.destroy() @staticmethod def format_path(representation): """Format file path correctly for single ass.* or ass.* sequence. Args: representation (dict): representation to be loaded. Returns: str: Formatted path to be used by the input node. """ path = get_representation_path(representation) if not os.path.exists(path): raise RuntimeError("Path does not exist: {}".format(path)) is_sequence = bool(representation["context"].get("frame")) # The path is either a single file or sequence in a folder. if is_sequence: dir_path, file_name = os.path.split(path) path = os.path.join( dir_path, re.sub(r"(.*)\.(\d+)\.(ass.*)", "\\1.$F4.\\3", file_name) ) return os.path.normpath(path).replace("\\", "/") def switch(self, container, representation): self.update(container, representation) ================================================ FILE: openpype/hosts/houdini/plugins/load/load_bgeo.py ================================================ # -*- coding: utf-8 -*- import os import re from openpype.pipeline import ( load, get_representation_path, ) from openpype.hosts.houdini.api import pipeline class BgeoLoader(load.LoaderPlugin): """Load bgeo files to Houdini.""" label = "Load bgeo" families = ["model", "pointcache", "bgeo"] representations = [ "bgeo", "bgeosc", "bgeogz", "bgeo.sc", "bgeo.gz", "bgeo.lzma", "bgeo.bz2"] order = -10 icon = "code-fork" color = "orange" def load(self, context, name=None, namespace=None, data=None): import hou # Get the root node obj = hou.node("/obj") # Define node name namespace = namespace if namespace else context["asset"]["name"] node_name = "{}_{}".format(namespace, name) if namespace else name # Create a new geo node container = obj.createNode("geo", node_name=node_name) # Remove the file node, it only loads static meshes # Houdini 17 has removed the file node from the geo node file_node = container.node("file1") if file_node: file_node.destroy() # Explicitly create a file node path = self.filepath_from_context(context) file_node = container.createNode("file", node_name=node_name) file_node.setParms( {"file": self.format_path(path, context["representation"])}) # Set display on last node file_node.setDisplayFlag(True) nodes = [container, file_node] self[:] = nodes return pipeline.containerise( node_name, namespace, nodes, context, self.__class__.__name__, suffix="", ) @staticmethod def format_path(path, representation): """Format file path correctly for single bgeo or bgeo sequence.""" if not os.path.exists(path): raise RuntimeError("Path does not exist: %s" % path) is_sequence = bool(representation["context"].get("frame")) # The path is either a single file or sequence in a folder. if not is_sequence: filename = path else: filename = re.sub(r"(.*)\.(\d+)\.(bgeo.*)", "\\1.$F4.\\3", path) filename = os.path.join(path, filename) filename = os.path.normpath(filename) filename = filename.replace("\\", "/") return filename def update(self, container, representation): node = container["node"] try: file_node = next( n for n in node.children() if n.type().name() == "file" ) except StopIteration: self.log.error("Could not find node of type `alembic`") return # Update the file path file_path = get_representation_path(representation) file_path = self.format_path(file_path, representation) file_node.setParms({"file": file_path}) # Update attribute node.setParms({"representation": str(representation["_id"])}) def remove(self, container): node = container["node"] node.destroy() def switch(self, container, representation): self.update(container, representation) ================================================ FILE: openpype/hosts/houdini/plugins/load/load_camera.py ================================================ from openpype.pipeline import ( load, get_representation_path, ) from openpype.hosts.houdini.api import pipeline from openpype.hosts.houdini.api.lib import ( set_camera_resolution, get_camera_from_container ) import hou ARCHIVE_EXPRESSION = ('__import__("_alembic_hom_extensions")' '.alembicGetCameraDict') def transfer_non_default_values(src, dest, ignore=None): """Copy parm from src to dest. Because the Alembic Archive rebuilds the entire node hierarchy on triggering "Build Hierarchy" we want to preserve any local tweaks made by the user on the camera for ease of use. That could be a background image, a resolution change or even Redshift camera parameters. We try to do so by finding all Parms that exist on both source and destination node, include only those that both are not at their default value, they must be visible, we exclude those that have the special "alembic archive" channel expression and ignore certain Parm types. """ ignore_types = { hou.parmTemplateType.Toggle, hou.parmTemplateType.Menu, hou.parmTemplateType.Button, hou.parmTemplateType.FolderSet, hou.parmTemplateType.Separator, hou.parmTemplateType.Label, } src.updateParmStates() for parm in src.allParms(): if ignore and parm.name() in ignore: continue # If destination parm does not exist, ignore.. dest_parm = dest.parm(parm.name()) if not dest_parm: continue # Ignore values that are currently at default if parm.isAtDefault() and dest_parm.isAtDefault(): continue if not parm.isVisible(): # Ignore hidden parameters, assume they # are implementation details continue expression = None try: expression = parm.expression() except hou.OperationFailed: # No expression present pass if expression is not None and ARCHIVE_EXPRESSION in expression: # Assume it's part of the automated connections that the # Alembic Archive makes on loading of the camera and thus we do # not want to transfer the expression continue # Ignore folders, separators, etc. if parm.parmTemplate().type() in ignore_types: continue print("Preserving attribute: %s" % parm.name()) dest_parm.setFromParm(parm) class CameraLoader(load.LoaderPlugin): """Load camera from an Alembic file""" families = ["camera"] label = "Load Camera (abc)" representations = ["abc"] order = -10 icon = "code-fork" color = "orange" def load(self, context, name=None, namespace=None, data=None): # Format file name, Houdini only wants forward slashes file_path = self.filepath_from_context(context).replace("\\", "/") # Get the root node obj = hou.node("/obj") # Define node name namespace = namespace if namespace else context["asset"]["name"] node_name = "{}_{}".format(namespace, name) if namespace else name # Create a archive node node = self.create_and_connect(obj, "alembicarchive", node_name) # TODO: add FPS of project / asset node.setParms({"fileName": file_path, "channelRef": True}) # Apply some magic node.parm("buildHierarchy").pressButton() node.moveToGoodPosition() # Create an alembic xform node nodes = [node] camera = get_camera_from_container(node) self._match_maya_render_mask(camera) set_camera_resolution(camera, asset_doc=context["asset"]) self[:] = nodes return pipeline.containerise(node_name, namespace, nodes, context, self.__class__.__name__, suffix="") def update(self, container, representation): node = container["node"] # Update the file path file_path = get_representation_path(representation) file_path = file_path.replace("\\", "/") # Update attributes node.setParms({"fileName": file_path, "representation": str(representation["_id"])}) # Store the cam temporarily next to the Alembic Archive # so that we can preserve parm values the user set on it # after build hierarchy was triggered. old_camera = get_camera_from_container(node) temp_camera = old_camera.copyTo(node.parent()) # Rebuild node.parm("buildHierarchy").pressButton() # Apply values to the new camera new_camera = get_camera_from_container(node) transfer_non_default_values(temp_camera, new_camera, # The hidden uniform scale attribute # gets a default connection to # "icon_scale" just skip that completely ignore={"scale"}) self._match_maya_render_mask(new_camera) set_camera_resolution(new_camera) temp_camera.destroy() def remove(self, container): node = container["node"] node.destroy() def create_and_connect(self, node, node_type, name=None): """Create a node within a node which and connect it to the input Args: node(hou.Node): parent of the new node node_type(str) name of the type of node, eg: 'alembic' name(str, Optional): name of the node Returns: hou.Node """ if name: new_node = node.createNode(node_type, node_name=name) else: new_node = node.createNode(node_type) new_node.moveToGoodPosition() return new_node def _match_maya_render_mask(self, camera): """Workaround to match Maya render mask in Houdini""" # print("Setting match maya render mask ") parm = camera.parm("aperture") expression = parm.expression() expression = expression.replace("return ", "aperture = ") expression += """ # Match maya render mask (logic from Houdini's own FBX importer) node = hou.pwd() resx = node.evalParm('resx') resy = node.evalParm('resy') aspect = node.evalParm('aspect') aperture *= min(1, (resx / resy * aspect) / 1.5) return aperture """ parm.setExpression(expression, language=hou.exprLanguage.Python) ================================================ FILE: openpype/hosts/houdini/plugins/load/load_fbx.py ================================================ # -*- coding: utf-8 -*- """Fbx Loader for houdini. """ from openpype.pipeline import ( load, get_representation_path, ) from openpype.hosts.houdini.api import pipeline class FbxLoader(load.LoaderPlugin): """Load fbx files. """ label = "Load FBX" icon = "code-fork" color = "orange" order = -10 families = ["*"] representations = ["*"] extensions = {"fbx"} def load(self, context, name=None, namespace=None, data=None): # get file path from context file_path = self.filepath_from_context(context) file_path = file_path.replace("\\", "/") # get necessary data namespace, node_name = self.get_node_name(context, name, namespace) # create load tree nodes = self.create_load_node_tree(file_path, node_name, name) self[:] = nodes # Call containerise function which does some automations for you # like moving created nodes to the AVALON_CONTAINERS subnetwork containerised_nodes = pipeline.containerise( node_name, namespace, nodes, context, self.__class__.__name__, suffix="", ) return containerised_nodes def update(self, container, representation): node = container["node"] try: file_node = next( n for n in node.children() if n.type().name() == "file" ) except StopIteration: self.log.error("Could not find node of type `file`") return # Update the file path from representation file_path = get_representation_path(representation) file_path = file_path.replace("\\", "/") file_node.setParms({"file": file_path}) # Update attribute node.setParms({"representation": str(representation["_id"])}) def remove(self, container): node = container["node"] node.destroy() def switch(self, container, representation): self.update(container, representation) def get_node_name(self, context, name=None, namespace=None): """Define node name.""" if not namespace: namespace = context["asset"]["name"] if namespace: node_name = "{}_{}".format(namespace, name) else: node_name = name return namespace, node_name def create_load_node_tree(self, file_path, node_name, subset_name): """Create Load network. you can start building your tree at any obj level. it'll be much easier to build it in the root obj level. Afterwards, your tree will be automatically moved to '/obj/AVALON_CONTAINERS' subnetwork. """ import hou # Get the root obj level obj = hou.node("/obj") # Create a new obj geo node parent_node = obj.createNode("geo", node_name=node_name) # In older houdini, # when reating a new obj geo node, a default file node will be # automatically created. # so, we will delete it if exists. file_node = parent_node.node("file1") if file_node: file_node.destroy() # Create a new file node file_node = parent_node.createNode("file", node_name=node_name) file_node.setParms({"file": file_path}) # Create attribute delete attribdelete_name = "attribdelete_{}".format(subset_name) attribdelete = parent_node.createNode("attribdelete", node_name=attribdelete_name) attribdelete.setParms({"ptdel": "fbx_*"}) attribdelete.setInput(0, file_node) # Create a Null node null_name = "OUT_{}".format(subset_name) null = parent_node.createNode("null", node_name=null_name) null.setInput(0, attribdelete) # Ensure display flag is on the file_node input node and not on the OUT # node to optimize "debug" displaying in the viewport. file_node.setDisplayFlag(True) # Set new position for children nodes parent_node.layoutChildren() # Return all the nodes return [parent_node, file_node, attribdelete, null] ================================================ FILE: openpype/hosts/houdini/plugins/load/load_hda.py ================================================ # -*- coding: utf-8 -*- import os from openpype.pipeline import ( load, get_representation_path, ) from openpype.hosts.houdini.api import pipeline class HdaLoader(load.LoaderPlugin): """Load Houdini Digital Asset file.""" families = ["hda"] label = "Load Hda" representations = ["hda"] order = -10 icon = "code-fork" color = "orange" def load(self, context, name=None, namespace=None, data=None): import hou # Format file name, Houdini only wants forward slashes file_path = self.filepath_from_context(context) file_path = os.path.normpath(file_path) file_path = file_path.replace("\\", "/") # Get the root node obj = hou.node("/obj") # Create a unique name counter = 1 namespace = namespace or context["asset"]["name"] formatted = "{}_{}".format(namespace, name) if namespace else name node_name = "{0}_{1:03d}".format(formatted, counter) hou.hda.installFile(file_path) hda_node = obj.createNode(name, node_name) self[:] = [hda_node] return pipeline.containerise( node_name, namespace, [hda_node], context, self.__class__.__name__, suffix="", ) def update(self, container, representation): import hou hda_node = container["node"] file_path = get_representation_path(representation) file_path = file_path.replace("\\", "/") hou.hda.installFile(file_path) defs = hda_node.type().allInstalledDefinitions() def_paths = [d.libraryFilePath() for d in defs] new = def_paths.index(file_path) defs[new].setIsPreferred(True) hda_node.setParms({ "representation": str(representation["_id"]) }) def remove(self, container): node = container["node"] node.destroy() ================================================ FILE: openpype/hosts/houdini/plugins/load/load_image.py ================================================ import os from openpype.pipeline import ( load, get_representation_path, AVALON_CONTAINER_ID, ) from openpype.hosts.houdini.api import lib, pipeline import hou def get_image_avalon_container(): """The COP2 files must be in a COP2 network. So we maintain a single entry point within AVALON_CONTAINERS, just for ease of use. """ path = pipeline.AVALON_CONTAINERS avalon_container = hou.node(path) if not avalon_container: # Let's create avalon container secretly # but make sure the pipeline still is built the # way we anticipate it was built, asserting it. assert path == "/obj/AVALON_CONTAINERS" parent = hou.node("/obj") avalon_container = parent.createNode( "subnet", node_name="AVALON_CONTAINERS" ) image_container = hou.node(path + "/IMAGES") if not image_container: image_container = avalon_container.createNode( "cop2net", node_name="IMAGES" ) image_container.moveToGoodPosition() return image_container class ImageLoader(load.LoaderPlugin): """Load images into COP2""" families = ["imagesequence"] label = "Load Image (COP2)" representations = ["*"] order = -10 icon = "code-fork" color = "orange" def load(self, context, name=None, namespace=None, data=None): # Format file name, Houdini only wants forward slashes file_path = self.filepath_from_context(context) file_path = os.path.normpath(file_path) file_path = file_path.replace("\\", "/") file_path = self._get_file_sequence(file_path) # Get the root node parent = get_image_avalon_container() # Define node name namespace = namespace if namespace else context["asset"]["name"] node_name = "{}_{}".format(namespace, name) if namespace else name node = parent.createNode("file", node_name=node_name) node.moveToGoodPosition() node.setParms({"filename1": file_path}) # Imprint it manually data = { "schema": "openpype:container-2.0", "id": AVALON_CONTAINER_ID, "name": node_name, "namespace": namespace, "loader": str(self.__class__.__name__), "representation": str(context["representation"]["_id"]), } # todo: add folder="Avalon" lib.imprint(node, data) return node def update(self, container, representation): node = container["node"] # Update the file path file_path = get_representation_path(representation) file_path = file_path.replace("\\", "/") file_path = self._get_file_sequence(file_path) # Update attributes node.setParms( { "filename1": file_path, "representation": str(representation["_id"]), } ) def remove(self, container): node = container["node"] # Let's clean up the IMAGES COP2 network # if it ends up being empty and we deleted # the last file node. Store the parent # before we delete the node. parent = node.parent() node.destroy() if not parent.children(): parent.destroy() def _get_file_sequence(self, file_path): root = os.path.dirname(file_path) files = sorted(os.listdir(root)) first_fname = files[0] prefix, padding, suffix = first_fname.rsplit(".", 2) fname = ".".join([prefix, "$F{}".format(len(padding)), suffix]) return os.path.join(root, fname).replace("\\", "/") def switch(self, container, representation): self.update(container, representation) ================================================ FILE: openpype/hosts/houdini/plugins/load/load_redshift_proxy.py ================================================ import os import re from openpype.pipeline import ( load, get_representation_path, ) from openpype.hosts.houdini.api import pipeline from openpype.pipeline.load import LoadError import hou class RedshiftProxyLoader(load.LoaderPlugin): """Load Redshift Proxy""" families = ["redshiftproxy"] label = "Load Redshift Proxy" representations = ["rs"] order = -10 icon = "code-fork" color = "orange" def load(self, context, name=None, namespace=None, data=None): # Get the root node obj = hou.node("/obj") # Define node name namespace = namespace if namespace else context["asset"]["name"] node_name = "{}_{}".format(namespace, name) if namespace else name # Create a new geo node container = obj.createNode("geo", node_name=node_name) # Check whether the Redshift parameters exist - if not, then likely # redshift is not set up or initialized correctly if not container.parm("RS_objprop_proxy_enable"): container.destroy() raise LoadError("Unable to initialize geo node with Redshift " "attributes. Make sure you have the Redshift " "plug-in set up correctly for Houdini.") # Enable by default container.setParms({ "RS_objprop_proxy_enable": True, "RS_objprop_proxy_file": self.format_path( self.filepath_from_context(context), context["representation"]) }) # Remove the file node, it only loads static meshes # Houdini 17 has removed the file node from the geo node file_node = container.node("file1") if file_node: file_node.destroy() # Add this stub node inside so it previews ok proxy_sop = container.createNode("redshift_proxySOP", node_name=node_name) proxy_sop.setDisplayFlag(True) nodes = [container, proxy_sop] self[:] = nodes return pipeline.containerise( node_name, namespace, nodes, context, self.__class__.__name__, suffix="", ) def update(self, container, representation): # Update the file path file_path = get_representation_path(representation) node = container["node"] node.setParms({ "RS_objprop_proxy_file": self.format_path( file_path, representation) }) # Update attribute node.setParms({"representation": str(representation["_id"])}) def remove(self, container): node = container["node"] node.destroy() @staticmethod def format_path(path, representation): """Format file path correctly for single redshift proxy or redshift proxy sequence.""" if not os.path.exists(path): raise RuntimeError("Path does not exist: %s" % path) is_sequence = bool(representation["context"].get("frame")) # The path is either a single file or sequence in a folder. if is_sequence: filename = re.sub(r"(.*)\.(\d+)\.(rs.*)", "\\1.$F4.\\3", path) filename = os.path.join(path, filename) else: filename = path filename = os.path.normpath(filename) filename = filename.replace("\\", "/") return filename ================================================ FILE: openpype/hosts/houdini/plugins/load/load_usd_layer.py ================================================ from openpype.pipeline import ( load, get_representation_path, AVALON_CONTAINER_ID, ) from openpype.hosts.houdini.api import lib class USDSublayerLoader(load.LoaderPlugin): """Sublayer USD file in Solaris""" families = [ "usd", "usdCamera", ] label = "Sublayer USD" representations = ["usd", "usda", "usdlc", "usdnc", "abc"] order = 1 icon = "code-fork" color = "orange" def load(self, context, name=None, namespace=None, data=None): import os import hou # Format file name, Houdini only wants forward slashes file_path = self.filepath_from_context(context) file_path = os.path.normpath(file_path) file_path = file_path.replace("\\", "/") # Get the root node stage = hou.node("/stage") # Define node name namespace = namespace if namespace else context["asset"]["name"] node_name = "{}_{}".format(namespace, name) if namespace else name # Create USD reference container = stage.createNode("sublayer", node_name=node_name) container.setParms({"filepath1": file_path}) container.moveToGoodPosition() # Imprint it manually data = { "schema": "openpype:container-2.0", "id": AVALON_CONTAINER_ID, "name": node_name, "namespace": namespace, "loader": str(self.__class__.__name__), "representation": str(context["representation"]["_id"]), } # todo: add folder="Avalon" lib.imprint(container, data) return container def update(self, container, representation): node = container["node"] # Update the file path file_path = get_representation_path(representation) file_path = file_path.replace("\\", "/") # Update attributes node.setParms( { "filepath1": file_path, "representation": str(representation["_id"]), } ) # Reload files node.parm("reload").pressButton() def remove(self, container): node = container["node"] node.destroy() def switch(self, container, representation): self.update(container, representation) ================================================ FILE: openpype/hosts/houdini/plugins/load/load_usd_reference.py ================================================ from openpype.pipeline import ( load, get_representation_path, AVALON_CONTAINER_ID, ) from openpype.hosts.houdini.api import lib class USDReferenceLoader(load.LoaderPlugin): """Reference USD file in Solaris""" families = [ "usd", "usdCamera", ] label = "Reference USD" representations = ["usd", "usda", "usdlc", "usdnc", "abc"] order = -8 icon = "code-fork" color = "orange" def load(self, context, name=None, namespace=None, data=None): import os import hou # Format file name, Houdini only wants forward slashes file_path = self.filepath_from_context(context) file_path = os.path.normpath(file_path) file_path = file_path.replace("\\", "/") # Get the root node stage = hou.node("/stage") # Define node name namespace = namespace if namespace else context["asset"]["name"] node_name = "{}_{}".format(namespace, name) if namespace else name # Create USD reference container = stage.createNode("reference", node_name=node_name) container.setParms({"filepath1": file_path}) container.moveToGoodPosition() # Imprint it manually data = { "schema": "openpype:container-2.0", "id": AVALON_CONTAINER_ID, "name": node_name, "namespace": namespace, "loader": str(self.__class__.__name__), "representation": str(context["representation"]["_id"]), } # todo: add folder="Avalon" lib.imprint(container, data) return container def update(self, container, representation): node = container["node"] # Update the file path file_path = get_representation_path(representation) file_path = file_path.replace("\\", "/") # Update attributes node.setParms( { "filepath1": file_path, "representation": str(representation["_id"]), } ) # Reload files node.parm("reload").pressButton() def remove(self, container): node = container["node"] node.destroy() def switch(self, container, representation): self.update(container, representation) ================================================ FILE: openpype/hosts/houdini/plugins/load/load_vdb.py ================================================ import os import re from openpype.pipeline import ( load, get_representation_path, ) from openpype.hosts.houdini.api import pipeline class VdbLoader(load.LoaderPlugin): """Load VDB""" families = ["vdbcache"] label = "Load VDB" representations = ["vdb"] order = -10 icon = "code-fork" color = "orange" def load(self, context, name=None, namespace=None, data=None): import hou # Get the root node obj = hou.node("/obj") # Define node name namespace = namespace if namespace else context["asset"]["name"] node_name = "{}_{}".format(namespace, name) if namespace else name # Create a new geo node container = obj.createNode("geo", node_name=node_name) # Remove the file node, it only loads static meshes # Houdini 17 has removed the file node from the geo node file_node = container.node("file1") if file_node: file_node.destroy() # Explicitly create a file node file_node = container.createNode("file", node_name=node_name) path = self.filepath_from_context(context) file_node.setParms( {"file": self.format_path(path, context["representation"])}) # Set display on last node file_node.setDisplayFlag(True) nodes = [container, file_node] self[:] = nodes return pipeline.containerise( node_name, namespace, nodes, context, self.__class__.__name__, suffix="", ) @staticmethod def format_path(path, representation): """Format file path correctly for single vdb or vdb sequence.""" if not os.path.exists(path): raise RuntimeError("Path does not exist: %s" % path) is_sequence = bool(representation["context"].get("frame")) # The path is either a single file or sequence in a folder. if not is_sequence: filename = path else: filename = re.sub(r"(.*)\.(\d+)\.vdb$", "\\1.$F4.vdb", path) filename = os.path.join(path, filename) filename = os.path.normpath(filename) filename = filename.replace("\\", "/") return filename def update(self, container, representation): node = container["node"] try: file_node = next( n for n in node.children() if n.type().name() == "file" ) except StopIteration: self.log.error("Could not find node of type `alembic`") return # Update the file path file_path = get_representation_path(representation) file_path = self.format_path(file_path, representation) file_node.setParms({"file": file_path}) # Update attribute node.setParms({"representation": str(representation["_id"])}) def remove(self, container): node = container["node"] node.destroy() def switch(self, container, representation): self.update(container, representation) ================================================ FILE: openpype/hosts/houdini/plugins/load/show_usdview.py ================================================ import os import platform import subprocess from openpype.lib.vendor_bin_utils import find_executable from openpype.pipeline import load class ShowInUsdview(load.LoaderPlugin): """Open USD file in usdview""" label = "Show in usdview" representations = ["*"] families = ["*"] extensions = {"usd", "usda", "usdlc", "usdnc", "abc"} order = 15 icon = "code-fork" color = "white" def load(self, context, name=None, namespace=None, data=None): from pathlib import Path if platform.system() == "Windows": executable = "usdview.bat" else: executable = "usdview" usdview = find_executable(executable) if not usdview: raise RuntimeError("Unable to find usdview") # For some reason Windows can return the path like: # C:/PROGRA~1/SIDEEF~1/HOUDIN~1.435/bin/usdview # convert to resolved path so `subprocess` can take it usdview = str(Path(usdview).resolve().as_posix()) filepath = self.filepath_from_context(context) filepath = os.path.normpath(filepath) filepath = filepath.replace("\\", "/") if not os.path.exists(filepath): self.log.error("File does not exist: %s" % filepath) return self.log.info("Start houdini variant of usdview...") subprocess.Popen([usdview, filepath, "--renderer", "GL"]) ================================================ FILE: openpype/hosts/houdini/plugins/publish/collect_active_state.py ================================================ import pyblish.api import hou class CollectInstanceActiveState(pyblish.api.InstancePlugin): """Collect default active state for instance from its node bypass state. This is done at the very end of the CollectorOrder so that any required collecting of data iterating over instances (with InstancePlugin) will actually collect the data for when the user enables the state in the UI. Otherwise potentially required data might have skipped collecting. """ order = pyblish.api.CollectorOrder + 0.299 families = ["*"] hosts = ["houdini"] label = "Instance Active State" def process(self, instance): # Must have node to check for bypass state if len(instance) == 0: return # Check bypass state and reverse active = True node = hou.node(instance.data.get("instance_node")) if hasattr(node, "isBypassed"): active = not node.isBypassed() # Set instance active state instance.data.update( { "active": active, # temporarily translation of `active` to `publish` till # issue has been resolved: # https://github.com/pyblish/pyblish-base/issues/307 "publish": active, } ) ================================================ FILE: openpype/hosts/houdini/plugins/publish/collect_arnold_rop.py ================================================ import os import re import hou import pyblish.api from openpype.hosts.houdini.api import colorspace from openpype.hosts.houdini.api.lib import ( evalParmNoFrame, get_color_management_preferences) class CollectArnoldROPRenderProducts(pyblish.api.InstancePlugin): """Collect Arnold ROP Render Products Collects the instance.data["files"] for the render products. Provides: instance -> files """ label = "Arnold ROP Render Products" # This specific order value is used so that # this plugin runs after CollectFrames order = pyblish.api.CollectorOrder + 0.11 hosts = ["houdini"] families = ["arnold_rop"] def process(self, instance): rop = hou.node(instance.data.get("instance_node")) # Collect chunkSize chunk_size_parm = rop.parm("chunkSize") if chunk_size_parm: chunk_size = int(chunk_size_parm.eval()) instance.data["chunkSize"] = chunk_size self.log.debug("Chunk Size: %s" % chunk_size) default_prefix = evalParmNoFrame(rop, "ar_picture") render_products = [] # Store whether we are splitting the render job (export + render) split_render = bool(rop.parm("ar_ass_export_enable").eval()) instance.data["splitRender"] = split_render export_prefix = None export_products = [] if split_render: export_prefix = evalParmNoFrame( rop, "ar_ass_file", pad_character="0" ) beauty_export_product = self.get_render_product_name( prefix=export_prefix, suffix=None) export_products.append(beauty_export_product) self.log.debug( "Found export product: {}".format(beauty_export_product) ) instance.data["ifdFile"] = beauty_export_product instance.data["exportFiles"] = list(export_products) # Default beauty AOV beauty_product = self.get_render_product_name(prefix=default_prefix, suffix=None) render_products.append(beauty_product) files_by_aov = { "": self.generate_expected_files(instance, beauty_product) } num_aovs = rop.evalParm("ar_aovs") for index in range(1, num_aovs + 1): # Skip disabled AOVs if not rop.evalParm("ar_enable_aov{}".format(index)): continue if rop.evalParm("ar_aov_exr_enable_layer_name{}".format(index)): label = rop.evalParm("ar_aov_exr_layer_name{}".format(index)) else: label = evalParmNoFrame(rop, "ar_aov_label{}".format(index)) aov_product = self.get_render_product_name(default_prefix, suffix=label) render_products.append(aov_product) files_by_aov[label] = self.generate_expected_files(instance, aov_product) for product in render_products: self.log.debug("Found render product: {}".format(product)) instance.data["files"] = list(render_products) instance.data["renderProducts"] = colorspace.ARenderProduct() # For now by default do NOT try to publish the rendered output instance.data["publishJobState"] = "Suspended" instance.data["attachTo"] = [] # stub required data if "expectedFiles" not in instance.data: instance.data["expectedFiles"] = list() instance.data["expectedFiles"].append(files_by_aov) # update the colorspace data colorspace_data = get_color_management_preferences() instance.data["colorspaceConfig"] = colorspace_data["config"] instance.data["colorspaceDisplay"] = colorspace_data["display"] instance.data["colorspaceView"] = colorspace_data["view"] def get_render_product_name(self, prefix, suffix): """Return the output filename using the AOV prefix and suffix""" # When AOV is explicitly defined in prefix we just swap it out # directly with the AOV suffix to embed it. # Note: ${AOV} seems to be evaluated in the parameter as %AOV% if "%AOV%" in prefix: # It seems that when some special separator characters are present # before the %AOV% token that Redshift will secretly remove it if # there is no suffix for the current product, for example: # foo_%AOV% -> foo.exr pattern = "%AOV%" if suffix else "[._-]?%AOV%" product_name = re.sub(pattern, suffix, prefix, flags=re.IGNORECASE) else: if suffix: # Add ".{suffix}" before the extension prefix_base, ext = os.path.splitext(prefix) product_name = prefix_base + "." + suffix + ext else: product_name = prefix return product_name def generate_expected_files(self, instance, path): """Create expected files in instance data""" dir = os.path.dirname(path) file = os.path.basename(path) if "#" in file: def replace(match): return "%0{}d".format(len(match.group())) file = re.sub("#+", replace, file) if "%" not in file: return path expected_files = [] start = instance.data["frameStartHandle"] end = instance.data["frameEndHandle"] for i in range(int(start), (int(end) + 1)): expected_files.append( os.path.join(dir, (file % i)).replace("\\", "/")) return expected_files ================================================ FILE: openpype/hosts/houdini/plugins/publish/collect_asset_handles.py ================================================ # -*- coding: utf-8 -*- """Collector plugin for frames data on ROP instances.""" import hou # noqa import pyblish.api from openpype.lib import BoolDef from openpype.pipeline import OpenPypePyblishPluginMixin class CollectAssetHandles(pyblish.api.InstancePlugin, OpenPypePyblishPluginMixin): """Apply asset handles. If instance does not have: - frameStart - frameEnd - handleStart - handleEnd But it does have: - frameStartHandle - frameEndHandle Then we will retrieve the asset's handles to compute the exclusive frame range and actual handle ranges. """ hosts = ["houdini"] # This specific order value is used so that # this plugin runs after CollectAnatomyInstanceData order = pyblish.api.CollectorOrder + 0.499 label = "Collect Asset Handles" use_asset_handles = True def process(self, instance): # Only process instances without already existing handles data # but that do have frameStartHandle and frameEndHandle defined # like the data collected from CollectRopFrameRange if "frameStartHandle" not in instance.data: return if "frameEndHandle" not in instance.data: return has_existing_data = { "handleStart", "handleEnd", "frameStart", "frameEnd" }.issubset(instance.data) if has_existing_data: return attr_values = self.get_attr_values_from_data(instance.data) if attr_values.get("use_handles", self.use_asset_handles): asset_data = instance.data["assetEntity"]["data"] handle_start = asset_data.get("handleStart", 0) handle_end = asset_data.get("handleEnd", 0) else: handle_start = 0 handle_end = 0 frame_start = instance.data["frameStartHandle"] + handle_start frame_end = instance.data["frameEndHandle"] - handle_end instance.data.update({ "handleStart": handle_start, "handleEnd": handle_end, "frameStart": frame_start, "frameEnd": frame_end }) # Log debug message about the collected frame range if attr_values.get("use_handles", self.use_asset_handles): self.log.debug( "Full Frame range with Handles " "[{frame_start_handle} - {frame_end_handle}]" .format( frame_start_handle=instance.data["frameStartHandle"], frame_end_handle=instance.data["frameEndHandle"] ) ) else: self.log.debug( "Use handles is deactivated for this instance, " "start and end handles are set to 0." ) # Log collected frame range to the user message = "Frame range [{frame_start} - {frame_end}]".format( frame_start=frame_start, frame_end=frame_end ) if handle_start or handle_end: message += " with handles [{handle_start}]-[{handle_end}]".format( handle_start=handle_start, handle_end=handle_end ) self.log.info(message) if instance.data.get("byFrameStep", 1.0) != 1.0: self.log.info( "Frame steps {}".format(instance.data["byFrameStep"])) # Add frame range to label if the instance has a frame range. label = instance.data.get("label", instance.data["name"]) instance.data["label"] = ( "{label} [{frame_start_handle} - {frame_end_handle}]" .format( label=label, frame_start_handle=instance.data["frameStartHandle"], frame_end_handle=instance.data["frameEndHandle"] ) ) @classmethod def get_attribute_defs(cls): return [ BoolDef("use_handles", tooltip="Disable this if you want the publisher to" " ignore start and end handles specified in the" " asset data for this publish instance", default=cls.use_asset_handles, label="Use asset handles") ] ================================================ FILE: openpype/hosts/houdini/plugins/publish/collect_cache_farm.py ================================================ import os import pyblish.api import hou from openpype.hosts.houdini.api import lib class CollectDataforCache(pyblish.api.InstancePlugin): """Collect data for caching to Deadline.""" order = pyblish.api.CollectorOrder + 0.04 families = ["ass", "pointcache", "mantraifd", "redshiftproxy", "vdbcache"] hosts = ["houdini"] targets = ["local", "remote"] label = "Collect Data for Cache" def process(self, instance): creator_attribute = instance.data["creator_attributes"] farm_enabled = creator_attribute["farm"] instance.data["farm"] = farm_enabled if not farm_enabled: self.log.debug("Caching on farm is disabled. " "Skipping farm collecting.") return # Why do we need this particular collector to collect the expected # output files from a ROP node. Don't we have a dedicated collector # for that yet? # Collect expected files ropnode = hou.node(instance.data["instance_node"]) output_parm = lib.get_output_parameter(ropnode) expected_filepath = output_parm.eval() instance.data.setdefault("files", list()) instance.data.setdefault("expectedFiles", list()) if instance.data.get("frames"): files = self.get_files(instance, expected_filepath) # list of files instance.data["files"].extend(files) else: # single file instance.data["files"].append(output_parm.eval()) cache_files = {"_": instance.data["files"]} # Convert instance family to pointcache if it is bgeo or abc # because ??? for family in instance.data["families"]: if family == "bgeo" or "abc": instance.data["family"] = "pointcache" break instance.data.update({ "plugin": "Houdini", "publish": True }) instance.data["families"].append("publish.hou") instance.data["expectedFiles"].append(cache_files) self.log.debug("{}".format(instance.data)) def get_files(self, instance, output_parm): """Get the files with the frame range data Args: instance (_type_): instance output_parm (_type_): path of output parameter Returns: files: a list of files """ directory = os.path.dirname(output_parm) files = [ os.path.join(directory, frame).replace("\\", "/") for frame in instance.data["frames"] ] return files ================================================ FILE: openpype/hosts/houdini/plugins/publish/collect_chunk_size.py ================================================ import pyblish.api from openpype.lib import NumberDef from openpype.pipeline import OpenPypePyblishPluginMixin class CollectChunkSize(pyblish.api.InstancePlugin, OpenPypePyblishPluginMixin): """Collect chunk size for cache submission to Deadline.""" order = pyblish.api.CollectorOrder + 0.05 families = ["ass", "pointcache", "vdbcache", "mantraifd", "redshiftproxy"] hosts = ["houdini"] targets = ["local", "remote"] label = "Collect Chunk Size" chunk_size = 999999 def process(self, instance): # need to get the chunk size info from the setting attr_values = self.get_attr_values_from_data(instance.data) instance.data["chunkSize"] = attr_values.get("chunkSize") @classmethod def get_attribute_defs(cls): return [ NumberDef("chunkSize", minimum=1, maximum=999999, decimals=0, default=cls.chunk_size, label="Frame Per Task") ] ================================================ FILE: openpype/hosts/houdini/plugins/publish/collect_current_file.py ================================================ import os import hou import pyblish.api class CollectHoudiniCurrentFile(pyblish.api.ContextPlugin): """Inject the current working file into context""" order = pyblish.api.CollectorOrder - 0.1 label = "Houdini Current File" hosts = ["houdini"] def process(self, context): """Inject the current working file""" current_file = hou.hipFile.path() if not os.path.exists(current_file): # By default, Houdini will even point a new scene to a path. # However if the file is not saved at all and does not exist, # we assume the user never set it. current_file = "" elif os.path.basename(current_file) == "untitled.hip": # Due to even a new file being called 'untitled.hip' we are unable # to confirm the current scene was ever saved because the file # could have existed already. We will allow it if the file exists, # but show a warning for this edge case to clarify the potential # false positive. self.log.warning( "Current file is 'untitled.hip' and we are " "unable to detect whether the current scene is " "saved correctly." ) context.data["currentFile"] = current_file self.log.info('Current workfile path: {}'.format(current_file)) ================================================ FILE: openpype/hosts/houdini/plugins/publish/collect_frames.py ================================================ # -*- coding: utf-8 -*- """Collector plugin for frames data on ROP instances.""" import os import re import hou # noqa import pyblish.api from openpype.hosts.houdini.api import lib class CollectFrames(pyblish.api.InstancePlugin): """Collect all frames which would be saved from the ROP nodes""" # This specific order value is used so that # this plugin runs after CollectRopFrameRange order = pyblish.api.CollectorOrder + 0.1 label = "Collect Frames" families = ["vdbcache", "imagesequence", "ass", "mantraifd", "redshiftproxy", "review", "bgeo"] def process(self, instance): ropnode = hou.node(instance.data["instance_node"]) start_frame = instance.data.get("frameStartHandle", None) end_frame = instance.data.get("frameEndHandle", None) output_parm = lib.get_output_parameter(ropnode) if start_frame is not None: # When rendering only a single frame still explicitly # get the name for that particular frame instead of current frame output = output_parm.evalAtFrame(start_frame) else: self.log.warning("Using current frame: {}".format(hou.frame())) output = output_parm.eval() _, ext = lib.splitext( output, allowed_multidot_extensions=[ ".ass.gz", ".bgeo.sc", ".bgeo.gz", ".bgeo.lzma", ".bgeo.bz2"]) file_name = os.path.basename(output) result = file_name # Get the filename pattern match from the output # path, so we can compute all frames that would # come out from rendering the ROP node if there # is a frame pattern in the name pattern = r"\w+\.(\d+)" + re.escape(ext) match = re.match(pattern, file_name) if match and start_frame is not None: # Check if frames are bigger than 1 (file collection) # override the result if end_frame - start_frame > 0: result = self.create_file_list( match, int(start_frame), int(end_frame) ) # todo: `frames` currently conflicts with "explicit frames" for a # for a custom frame list. So this should be refactored. instance.data.update({"frames": result}) @staticmethod def create_file_list(match, start_frame, end_frame): """Collect files based on frame range and `regex.match` Args: match(re.match): match object start_frame(int): start of the animation end_frame(int): end of the animation Returns: list """ # Get the padding length frame = match.group(1) padding = len(frame) # Get the parts of the filename surrounding the frame number, # so we can put our own frame numbers in. span = match.span(1) prefix = match.string[: span[0]] suffix = match.string[span[1]:] # Generate filenames for all frames result = [] for i in range(start_frame, end_frame + 1): # Format frame number by the padding amount str_frame = "{number:0{width}d}".format(number=i, width=padding) file_name = prefix + str_frame + suffix result.append(file_name) return result ================================================ FILE: openpype/hosts/houdini/plugins/publish/collect_inputs.py ================================================ import pyblish.api from openpype.pipeline import registered_host def collect_input_containers(nodes): """Collect containers that contain any of the node in `nodes`. This will return any loaded Avalon container that contains at least one of the nodes. As such, the Avalon container is an input for it. Or in short, there are member nodes of that container. Returns: list: Input avalon containers """ # Lookup by node ids lookup = frozenset(nodes) containers = [] host = registered_host() for container in host.ls(): node = container["node"] # Usually the loaded containers don't have any complex references # and the contained children should be all we need. So we disregard # checking for .references() on the nodes. members = set(node.allSubChildren()) members.add(node) # include the node itself # If there's an intersection if not lookup.isdisjoint(members): containers.append(container) return containers def iter_upstream(node): """Yields all upstream inputs for the current node. This includes all `node.inputAncestors()` but also traverses through all `node.references()` for the node itself and for any of the upstream nodes. This method has no max-depth and will collect all upstream inputs. Yields: hou.Node: The upstream nodes, including references. """ upstream = node.inputAncestors( include_ref_inputs=True, follow_subnets=True ) # Initialize process queue with the node's ancestors itself queue = list(upstream) collected = set(upstream) # Traverse upstream references for all nodes and yield them as we # process the queue. while queue: upstream_node = queue.pop() yield upstream_node # Find its references that are not collected yet. references = upstream_node.references() references = [n for n in references if n not in collected] queue.extend(references) collected.update(references) # Include the references' ancestors that have not been collected yet. for reference in references: ancestors = reference.inputAncestors( include_ref_inputs=True, follow_subnets=True ) ancestors = [n for n in ancestors if n not in collected] queue.extend(ancestors) collected.update(ancestors) class CollectUpstreamInputs(pyblish.api.InstancePlugin): """Collect source input containers used for this publish. This will include `inputs` data of which loaded publishes were used in the generation of this publish. This leaves an upstream trace to what was used as input. """ label = "Collect Inputs" order = pyblish.api.CollectorOrder + 0.4 hosts = ["houdini"] def process(self, instance): # We can't get the "inputAncestors" directly from the ROP # node, so we find the related output node (set in SOP/COP path) # and include that together with its ancestors output = instance.data.get("output_node") if output is None: # If no valid output node is set then ignore it as validation # will be checking those cases. self.log.debug( "No output node found, skipping collecting of inputs.." ) return # Collect all upstream parents nodes = list(iter_upstream(output)) nodes.append(output) # Collect containers for the given set of nodes containers = collect_input_containers(nodes) inputs = [c["representation"] for c in containers] instance.data["inputRepresentations"] = inputs self.log.debug("Collected inputs: %s" % inputs) ================================================ FILE: openpype/hosts/houdini/plugins/publish/collect_instances.py ================================================ import hou import pyblish.api from openpype.hosts.houdini.api import lib class CollectInstances(pyblish.api.ContextPlugin): """Gather instances by all node in out graph and pre-defined attributes This collector takes into account assets that are associated with an specific node and marked with a unique identifier; Identifier: id (str): "pyblish.avalon.instance Specific node: The specific node is important because it dictates in which way the subset is being exported. alembic: will export Alembic file which supports cascading attributes like 'cbId' and 'path' geometry: Can export a wide range of file types, default out """ order = pyblish.api.CollectorOrder - 0.01 label = "Collect Instances" hosts = ["houdini"] def process(self, context): nodes = hou.node("/out").children() nodes += hou.node("/obj").children() # Include instances in USD stage only when it exists so it # remains backwards compatible with version before houdini 18 stage = hou.node("/stage") if stage: nodes += stage.recursiveGlob("*", filter=hou.nodeTypeFilter.Rop) for node in nodes: if not node.parm("id"): continue if node.evalParm("id") != "pyblish.avalon.instance": continue # instance was created by new creator code, skip it as # it is already collected. if node.parm("creator_identifier"): continue has_family = node.evalParm("family") assert has_family, "'%s' is missing 'family'" % node.name() self.log.info( "Processing legacy instance node {}".format(node.path()) ) data = lib.read(node) # Check bypass state and reverse if hasattr(node, "isBypassed"): data.update({"active": not node.isBypassed()}) # temporarily translation of `active` to `publish` till issue has # been resolved. # https://github.com/pyblish/pyblish-base/issues/307 if "active" in data: data["publish"] = data["active"] # Create nice name if the instance has a frame range. label = data.get("name", node.name()) label += " (%s)" % data["asset"] # include asset in name instance = context.create_instance(label) # Include `families` using `family` data instance.data["families"] = [instance.data["family"]] instance[:] = [node] instance.data["instance_node"] = node.path() instance.data.update(data) def sort_by_family(instance): """Sort by family""" return instance.data.get("families", instance.data.get("family")) # Sort/grouped by family (preserving local index) context[:] = sorted(context, key=sort_by_family) return context ================================================ FILE: openpype/hosts/houdini/plugins/publish/collect_instances_usd_layered.py ================================================ import hou import pyblish.api from openpype.hosts.houdini.api import lib import openpype.hosts.houdini.api.usd as hou_usdlib import openpype.lib.usdlib as usdlib class CollectInstancesUsdLayered(pyblish.api.ContextPlugin): """Collect Instances from a ROP Network and its configured layer paths. The output nodes of the ROP node will only be published when *any* of the layers remain set to 'publish' by the user. This works differently from most of our Avalon instances in the pipeline. As opposed to storing `pyblish.avalon.instance` as id on the node we store `pyblish.avalon.usdlayered`. Additionally this instance has no need for storing family, asset, subset or name on the nodes. Instead all information is retrieved solely from the output filepath, which is an Avalon URI: avalon://{asset}/{subset}.{representation} Each final ROP node is considered a dependency for any of the Configured Save Path layers it sets along the way. As such, the instances shown in the Pyblish UI are solely the configured layers. The encapsulating usd files are generated whenever *any* of the dependencies is published. These dependency instances are stored in: instance.data["publishDependencies"] """ order = pyblish.api.CollectorOrder - 0.01 label = "Collect Instances (USD Configured Layers)" hosts = ["houdini"] def process(self, context): stage = hou.node("/stage") if not stage: # Likely Houdini version <18 return nodes = stage.recursiveGlob("*", filter=hou.nodeTypeFilter.Rop) for node in nodes: if not node.parm("id"): continue if node.evalParm("id") != "pyblish.avalon.usdlayered": continue has_family = node.evalParm("family") assert has_family, "'%s' is missing 'family'" % node.name() self.process_node(node, context) def sort_by_family(instance): """Sort by family""" return instance.data.get("families", instance.data.get("family")) # Sort/grouped by family (preserving local index) context[:] = sorted(context, key=sort_by_family) return context def process_node(self, node, context): # Allow a single ROP node or a full ROP network of USD ROP nodes # to be processed as a single entry that should "live together" on # a publish. if node.type().name() == "ropnet": # All rop nodes inside ROP Network ropnodes = node.recursiveGlob("*", filter=hou.nodeTypeFilter.Rop) else: # A single node ropnodes = [node] data = lib.read(node) # Don't use the explicit "colorbleed.usd.layered" family for publishing # instead use the "colorbleed.usd" family to integrate. data["publishFamilies"] = ["colorbleed.usd"] # For now group ALL of them into USD Layer subset group # Allow this subset to be grouped into a USD Layer on creation data["subsetGroup"] = "USD Layer" instances = list() dependencies = [] for ropnode in ropnodes: # Create a dependency instance per ROP Node. lopoutput = ropnode.evalParm("lopoutput") dependency_save_data = self.get_save_data(lopoutput) dependency = context.create_instance(dependency_save_data["name"]) dependency.append(ropnode) dependency.data.update(data) dependency.data.update(dependency_save_data) dependency.data["family"] = "colorbleed.usd.dependency" dependency.data["optional"] = False dependencies.append(dependency) # Hide the dependency instance from the context context.pop() # Get all configured layers for this USD ROP node # and create a Pyblish instance for each one layers = hou_usdlib.get_configured_save_layers(ropnode) for layer in layers: save_path = hou_usdlib.get_layer_save_path(layer) save_data = self.get_save_data(save_path) if not save_data: continue self.log.info(save_path) instance = context.create_instance(save_data["name"]) instance[:] = [node] # Set the instance data instance.data.update(data) instance.data.update(save_data) instance.data["usdLayer"] = layer instances.append(instance) # Store the collected ROP node dependencies self.log.debug("Collected dependencies: %s" % (dependencies,)) for instance in instances: instance.data["publishDependencies"] = dependencies def get_save_data(self, save_path): # Resolve Avalon URI uri_data = usdlib.parse_avalon_uri(save_path) if not uri_data: self.log.warning("Non Avalon URI Layer Path: %s" % save_path) return {} # Collect asset + subset from URI name = "{subset} ({asset})".format(**uri_data) fname = "{asset}_{subset}.{ext}".format(**uri_data) data = dict(uri_data) data["usdSavePath"] = save_path data["usdFilename"] = fname data["name"] = name return data ================================================ FILE: openpype/hosts/houdini/plugins/publish/collect_karma_rop.py ================================================ import re import os import hou import pyblish.api from openpype.hosts.houdini.api.lib import ( evalParmNoFrame, get_color_management_preferences ) from openpype.hosts.houdini.api import ( colorspace ) class CollectKarmaROPRenderProducts(pyblish.api.InstancePlugin): """Collect Karma Render Products Collects the instance.data["files"] for the multipart render product. Provides: instance -> files """ label = "Karma ROP Render Products" # This specific order value is used so that # this plugin runs after CollectFrames order = pyblish.api.CollectorOrder + 0.11 hosts = ["houdini"] families = ["karma_rop"] def process(self, instance): rop = hou.node(instance.data.get("instance_node")) # Collect chunkSize chunk_size_parm = rop.parm("chunkSize") if chunk_size_parm: chunk_size = int(chunk_size_parm.eval()) instance.data["chunkSize"] = chunk_size self.log.debug("Chunk Size: %s" % chunk_size) default_prefix = evalParmNoFrame(rop, "picture") render_products = [] # Default beauty AOV beauty_product = self.get_render_product_name( prefix=default_prefix, suffix=None ) render_products.append(beauty_product) files_by_aov = { "beauty": self.generate_expected_files(instance, beauty_product) } filenames = list(render_products) instance.data["files"] = filenames instance.data["renderProducts"] = colorspace.ARenderProduct() for product in render_products: self.log.debug("Found render product: %s" % product) if "expectedFiles" not in instance.data: instance.data["expectedFiles"] = list() instance.data["expectedFiles"].append(files_by_aov) # update the colorspace data colorspace_data = get_color_management_preferences() instance.data["colorspaceConfig"] = colorspace_data["config"] instance.data["colorspaceDisplay"] = colorspace_data["display"] instance.data["colorspaceView"] = colorspace_data["view"] def get_render_product_name(self, prefix, suffix): product_name = prefix if suffix: # Add ".{suffix}" before the extension prefix_base, ext = os.path.splitext(prefix) product_name = "{}.{}{}".format(prefix_base, suffix, ext) return product_name def generate_expected_files(self, instance, path): """Create expected files in instance data""" dir = os.path.dirname(path) file = os.path.basename(path) if "#" in file: def replace(match): return "%0{}d".format(len(match.group())) file = re.sub("#+", replace, file) if "%" not in file: return path expected_files = [] start = instance.data["frameStartHandle"] end = instance.data["frameEndHandle"] for i in range(int(start), (int(end) + 1)): expected_files.append( os.path.join(dir, (file % i)).replace("\\", "/")) return expected_files ================================================ FILE: openpype/hosts/houdini/plugins/publish/collect_mantra_rop.py ================================================ import re import os import hou import pyblish.api from openpype.hosts.houdini.api.lib import ( evalParmNoFrame, get_color_management_preferences ) from openpype.hosts.houdini.api import ( colorspace ) class CollectMantraROPRenderProducts(pyblish.api.InstancePlugin): """Collect Mantra Render Products Collects the instance.data["files"] for the render products. Provides: instance -> files """ label = "Mantra ROP Render Products" # This specific order value is used so that # this plugin runs after CollectFrames order = pyblish.api.CollectorOrder + 0.11 hosts = ["houdini"] families = ["mantra_rop"] def process(self, instance): rop = hou.node(instance.data.get("instance_node")) # Collect chunkSize chunk_size_parm = rop.parm("chunkSize") if chunk_size_parm: chunk_size = int(chunk_size_parm.eval()) instance.data["chunkSize"] = chunk_size self.log.debug("Chunk Size: %s" % chunk_size) default_prefix = evalParmNoFrame(rop, "vm_picture") render_products = [] # Store whether we are splitting the render job (export + render) split_render = bool(rop.parm("soho_outputmode").eval()) instance.data["splitRender"] = split_render export_prefix = None export_products = [] if split_render: export_prefix = evalParmNoFrame( rop, "soho_diskfile", pad_character="0" ) beauty_export_product = self.get_render_product_name( prefix=export_prefix, suffix=None) export_products.append(beauty_export_product) self.log.debug( "Found export product: {}".format(beauty_export_product) ) instance.data["ifdFile"] = beauty_export_product instance.data["exportFiles"] = list(export_products) # Default beauty AOV beauty_product = self.get_render_product_name( prefix=default_prefix, suffix=None ) render_products.append(beauty_product) files_by_aov = { "beauty": self.generate_expected_files(instance, beauty_product) } aov_numbers = rop.evalParm("vm_numaux") if aov_numbers > 0: # get the filenames of the AOVs for i in range(1, aov_numbers + 1): var = rop.evalParm("vm_variable_plane%d" % i) if var: aov_name = "vm_filename_plane%d" % i aov_boolean = "vm_usefile_plane%d" % i aov_enabled = rop.evalParm(aov_boolean) has_aov_path = rop.evalParm(aov_name) if has_aov_path and aov_enabled == 1: aov_prefix = evalParmNoFrame(rop, aov_name) aov_product = self.get_render_product_name( prefix=aov_prefix, suffix=None ) render_products.append(aov_product) files_by_aov[var] = self.generate_expected_files(instance, aov_product) # noqa for product in render_products: self.log.debug("Found render product: %s" % product) filenames = list(render_products) instance.data["files"] = filenames instance.data["renderProducts"] = colorspace.ARenderProduct() # For now by default do NOT try to publish the rendered output instance.data["publishJobState"] = "Suspended" instance.data["attachTo"] = [] # stub required data if "expectedFiles" not in instance.data: instance.data["expectedFiles"] = list() instance.data["expectedFiles"].append(files_by_aov) # update the colorspace data colorspace_data = get_color_management_preferences() instance.data["colorspaceConfig"] = colorspace_data["config"] instance.data["colorspaceDisplay"] = colorspace_data["display"] instance.data["colorspaceView"] = colorspace_data["view"] def get_render_product_name(self, prefix, suffix): product_name = prefix if suffix: # Add ".{suffix}" before the extension prefix_base, ext = os.path.splitext(prefix) product_name = prefix_base + "." + suffix + ext return product_name def generate_expected_files(self, instance, path): """Create expected files in instance data""" dir = os.path.dirname(path) file = os.path.basename(path) if "#" in file: def replace(match): return "%0{}d".format(len(match.group())) file = re.sub("#+", replace, file) if "%" not in file: return path expected_files = [] start = instance.data["frameStartHandle"] end = instance.data["frameEndHandle"] for i in range(int(start), (int(end) + 1)): expected_files.append( os.path.join(dir, (file % i)).replace("\\", "/")) return expected_files ================================================ FILE: openpype/hosts/houdini/plugins/publish/collect_output_node.py ================================================ import pyblish.api from openpype.pipeline.publish import KnownPublishError class CollectOutputSOPPath(pyblish.api.InstancePlugin): """Collect the out node's SOP/COP Path value.""" order = pyblish.api.CollectorOrder families = [ "pointcache", "camera", "vdbcache", "imagesequence", "usd", "usdrender", "redshiftproxy", "staticMesh" ] hosts = ["houdini"] label = "Collect Output Node Path" def process(self, instance): import hou node = hou.node(instance.data["instance_node"]) # Get sop path node_type = node.type().name() if node_type == "geometry": out_node = node.parm("soppath").evalAsNode() elif node_type == "alembic": # Alembic can switch between using SOP Path or object if node.parm("use_sop_path").eval(): out_node = node.parm("sop_path").evalAsNode() else: root = node.parm("root").eval() objects = node.parm("objects").eval() path = root + "/" + objects out_node = hou.node(path) elif node_type == "comp": out_node = node.parm("coppath").evalAsNode() elif node_type == "usd" or node_type == "usdrender": out_node = node.parm("loppath").evalAsNode() elif node_type == "usd_rop" or node_type == "usdrender_rop": # Inside Solaris e.g. /stage (not in ROP context) # When incoming connection is present it takes it directly inputs = node.inputs() if inputs: out_node = inputs[0] else: out_node = node.parm("loppath").evalAsNode() elif node_type == "Redshift_Proxy_Output": out_node = node.parm("RS_archive_sopPath").evalAsNode() elif node_type == "filmboxfbx": out_node = node.parm("startnode").evalAsNode() else: raise KnownPublishError( "ROP node type '{}' is not supported.".format(node_type) ) if not out_node: self.log.warning("No output node collected.") return self.log.debug("Output node: %s" % out_node.path()) instance.data["output_node"] = out_node ================================================ FILE: openpype/hosts/houdini/plugins/publish/collect_pointcache_type.py ================================================ """Collector for pointcache types. This will add additional family to pointcache instance based on the creator_identifier parameter. """ import pyblish.api class CollectPointcacheType(pyblish.api.InstancePlugin): """Collect data type for pointcache instance.""" order = pyblish.api.CollectorOrder hosts = ["houdini"] families = ["pointcache"] label = "Collect type of pointcache" def process(self, instance): if instance.data["creator_identifier"] == "io.openpype.creators.houdini.bgeo": # noqa: E501 instance.data["families"] += ["bgeo"] elif instance.data["creator_identifier"] == "io.openpype.creators.houdini.pointcache": # noqa: E501 instance.data["families"] += ["abc"] ================================================ FILE: openpype/hosts/houdini/plugins/publish/collect_redshift_rop.py ================================================ import re import os import hou import pyblish.api from openpype.hosts.houdini.api.lib import ( evalParmNoFrame, get_color_management_preferences ) from openpype.hosts.houdini.api import ( colorspace ) class CollectRedshiftROPRenderProducts(pyblish.api.InstancePlugin): """Collect USD Render Products Collects the instance.data["files"] for the render products. Provides: instance -> files """ label = "Redshift ROP Render Products" # This specific order value is used so that # this plugin runs after CollectFrames order = pyblish.api.CollectorOrder + 0.11 hosts = ["houdini"] families = ["redshift_rop"] def process(self, instance): rop = hou.node(instance.data.get("instance_node")) # Collect chunkSize chunk_size_parm = rop.parm("chunkSize") if chunk_size_parm: chunk_size = int(chunk_size_parm.eval()) instance.data["chunkSize"] = chunk_size self.log.debug("Chunk Size: %s" % chunk_size) default_prefix = evalParmNoFrame(rop, "RS_outputFileNamePrefix") beauty_suffix = rop.evalParm("RS_outputBeautyAOVSuffix") # Store whether we are splitting the render job (export + render) split_render = bool(rop.parm("RS_archive_enable").eval()) instance.data["splitRender"] = split_render export_products = [] if split_render: export_prefix = evalParmNoFrame( rop, "RS_archive_file", pad_character="0" ) beauty_export_product = self.get_render_product_name( prefix=export_prefix, suffix=None) export_products.append(beauty_export_product) self.log.debug( "Found export product: {}".format(beauty_export_product) ) instance.data["ifdFile"] = beauty_export_product instance.data["exportFiles"] = list(export_products) # Default beauty AOV beauty_product = self.get_render_product_name( prefix=default_prefix, suffix=beauty_suffix ) render_products = [beauty_product] files_by_aov = { "_": self.generate_expected_files(instance, beauty_product)} num_aovs = rop.evalParm("RS_aov") for index in range(num_aovs): i = index + 1 # Skip disabled AOVs if not rop.evalParm(f"RS_aovEnable_{i}"): continue aov_suffix = rop.evalParm(f"RS_aovSuffix_{i}") aov_prefix = evalParmNoFrame(rop, f"RS_aovCustomPrefix_{i}") if not aov_prefix: aov_prefix = default_prefix aov_product = self.get_render_product_name(aov_prefix, aov_suffix) render_products.append(aov_product) files_by_aov[aov_suffix] = self.generate_expected_files(instance, aov_product) # noqa for product in render_products: self.log.debug("Found render product: %s" % product) filenames = list(render_products) instance.data["files"] = filenames instance.data["renderProducts"] = colorspace.ARenderProduct() # For now by default do NOT try to publish the rendered output instance.data["publishJobState"] = "Suspended" instance.data["attachTo"] = [] # stub required data if "expectedFiles" not in instance.data: instance.data["expectedFiles"] = [] instance.data["expectedFiles"].append(files_by_aov) # update the colorspace data colorspace_data = get_color_management_preferences() instance.data["colorspaceConfig"] = colorspace_data["config"] instance.data["colorspaceDisplay"] = colorspace_data["display"] instance.data["colorspaceView"] = colorspace_data["view"] def get_render_product_name(self, prefix, suffix): """Return the output filename using the AOV prefix and suffix""" # When AOV is explicitly defined in prefix we just swap it out # directly with the AOV suffix to embed it. # Note: ${AOV} seems to be evaluated in the parameter as %AOV% has_aov_in_prefix = "%AOV%" in prefix if has_aov_in_prefix: # It seems that when some special separator characters are present # before the %AOV% token that Redshift will secretly remove it if # there is no suffix for the current product, for example: # foo_%AOV% -> foo.exr pattern = "%AOV%" if suffix else "[._-]?%AOV%" product_name = re.sub(pattern, suffix, prefix, flags=re.IGNORECASE) else: if suffix: # Add ".{suffix}" before the extension prefix_base, ext = os.path.splitext(prefix) product_name = prefix_base + "." + suffix + ext else: product_name = prefix return product_name def generate_expected_files(self, instance, path): """Create expected files in instance data""" dir = os.path.dirname(path) file = os.path.basename(path) if "#" in file: def replace(match): return "%0{}d".format(len(match.group())) file = re.sub("#+", replace, file) if "%" not in file: return path expected_files = [] start = instance.data["frameStartHandle"] end = instance.data["frameEndHandle"] for i in range(int(start), (int(end) + 1)): expected_files.append( os.path.join(dir, (file % i)).replace("\\", "/")) return expected_files ================================================ FILE: openpype/hosts/houdini/plugins/publish/collect_remote_publish.py ================================================ import pyblish.api import hou from openpype.pipeline.publish import RepairAction from openpype.hosts.houdini.api import lib class CollectRemotePublishSettings(pyblish.api.ContextPlugin): """Collect custom settings of the Remote Publish node.""" order = pyblish.api.CollectorOrder families = ["*"] hosts = ["houdini"] targets = ["deadline"] label = "Remote Publish Submission Settings" actions = [RepairAction] def process(self, context): node = hou.node("/out/REMOTE_PUBLISH") if not node: return attributes = lib.read(node) # Debug the settings we have collected for key, value in sorted(attributes.items()): self.log.debug("Collected %s: %s" % (key, value)) context.data.update(attributes) ================================================ FILE: openpype/hosts/houdini/plugins/publish/collect_render_products.py ================================================ import re import os import hou import pxr.UsdRender import pyblish.api def get_var_changed(variable=None): """Return changed variables and operators that use it. Note: `varchange` hscript states that it forces a recook of the nodes that use Variables. That was tested in Houdini 18.0.391. Args: variable (str, Optional): A specific variable to query the operators for. When None is provided it will return all variables that have had recent changes and require a recook. Defaults to None. Returns: dict: Variable that changed with the operators that use it. """ cmd = "varchange -V" if variable: cmd += " {0}".format(variable) output, _ = hou.hscript(cmd) changed = {} for line in output.split("Variable: "): if not line.strip(): continue split = line.split() var = split[0] operators = split[1:] changed[var] = operators return changed class CollectRenderProducts(pyblish.api.InstancePlugin): """Collect USD Render Products.""" label = "Collect Render Products" order = pyblish.api.CollectorOrder + 0.4 hosts = ["houdini"] families = ["usdrender"] def process(self, instance): node = instance.data.get("output_node") if not node: rop_path = instance.data["instance_node"].path() raise RuntimeError( "No output node found. Make sure to connect an " "input to the USD ROP: %s" % rop_path ) # Workaround Houdini 18.0.391 bug where $HIPNAME doesn't automatically # update after scene save. if hou.applicationVersion() == (18, 0, 391): self.log.debug( "Checking for recook to workaround " "$HIPNAME refresh bug..." ) changed = get_var_changed("HIPNAME").get("HIPNAME") if changed: self.log.debug("Recooking for $HIPNAME refresh bug...") for operator in changed: hou.node(operator).cook(force=True) # Make sure to recook any 'cache' nodes in the history chain chain = [node] chain.extend(node.inputAncestors()) for input_node in chain: if input_node.type().name() == "cache": input_node.cook(force=True) stage = node.stage() filenames = [] for prim in stage.Traverse(): if not prim.IsA(pxr.UsdRender.Product): continue # Get Render Product Name product = pxr.UsdRender.Product(prim) # We force taking it from any random time sample as opposed to # "default" that the USD Api falls back to since that won't return # time sampled values if they were set per time sample. name = product.GetProductNameAttr().Get(time=0) dirname = os.path.dirname(name) basename = os.path.basename(name) dollarf_regex = r"(\$F([0-9]?))" frame_regex = r"^(.+\.)([0-9]+)(\.[a-zA-Z]+)$" if re.match(dollarf_regex, basename): # TODO: Confirm this actually is allowed USD stages and HUSK # Substitute $F def replace(match): """Replace $F4 with padded #.""" padding = int(match.group(2)) if match.group(2) else 1 return "#" * padding filename_base = re.sub(dollarf_regex, replace, basename) filename = os.path.join(dirname, filename_base) else: # Substitute basename.0001.ext def replace(match): prefix, frame, ext = match.groups() padding = "#" * len(frame) return prefix + padding + ext filename_base = re.sub(frame_regex, replace, basename) filename = os.path.join(dirname, filename_base) filename = filename.replace("\\", "/") assert "#" in filename, ( "Couldn't resolve render product name " "with frame number: %s" % name ) filenames.append(filename) prim_path = str(prim.GetPath()) self.log.info("Collected %s name: %s" % (prim_path, filename)) # Filenames for Deadline instance.data["files"] = filenames ================================================ FILE: openpype/hosts/houdini/plugins/publish/collect_review_data.py ================================================ import hou import pyblish.api class CollectHoudiniReviewData(pyblish.api.InstancePlugin): """Collect Review Data.""" label = "Collect Review Data" # This specific order value is used so that # this plugin runs after CollectRopFrameRange order = pyblish.api.CollectorOrder + 0.1 hosts = ["houdini"] families = ["review"] def process(self, instance): # This fixes the burnin having the incorrect start/end timestamps # because without this it would take it from the context instead # which isn't the actual frame range that this instance renders. instance.data["handleStart"] = 0 instance.data["handleEnd"] = 0 instance.data["fps"] = instance.context.data["fps"] # Enable ftrack functionality instance.data.setdefault("families", []).append('ftrack') # Get the camera from the rop node to collect the focal length ropnode_path = instance.data["instance_node"] ropnode = hou.node(ropnode_path) camera_path = ropnode.parm("camera").eval() camera_node = hou.node(camera_path) if not camera_node: self.log.warning("No valid camera node found on review node: " "{}".format(camera_path)) return # Collect focal length. focal_length_parm = camera_node.parm("focal") if not focal_length_parm: self.log.warning("No 'focal' (focal length) parameter found on " "camera: {}".format(camera_path)) return if focal_length_parm.isTimeDependent(): start = instance.data["frameStartHandle"] end = instance.data["frameEndHandle"] + 1 focal_length = [ focal_length_parm.evalAsFloatAtFrame(t) for t in range(int(start), int(end)) ] else: focal_length = focal_length_parm.evalAsFloat() # Store focal length in `burninDataMembers` burnin_members = instance.data.setdefault("burninDataMembers", {}) burnin_members["focalLength"] = focal_length ================================================ FILE: openpype/hosts/houdini/plugins/publish/collect_rop_frame_range.py ================================================ # -*- coding: utf-8 -*- """Collector plugin for frames data on ROP instances.""" import hou # noqa import pyblish.api from openpype.hosts.houdini.api import lib class CollectRopFrameRange(pyblish.api.InstancePlugin): """Collect all frames which would be saved from the ROP nodes""" hosts = ["houdini"] order = pyblish.api.CollectorOrder label = "Collect RopNode Frame Range" def process(self, instance): node_path = instance.data.get("instance_node") if node_path is None: # Instance without instance node like a workfile instance self.log.debug( "No instance node found for instance: {}".format(instance) ) return ropnode = hou.node(node_path) frame_data = lib.get_frame_data( ropnode, self.log ) if not frame_data: return # Log debug message about the collected frame range self.log.debug( "Collected frame_data: {}".format(frame_data) ) instance.data.update(frame_data) ================================================ FILE: openpype/hosts/houdini/plugins/publish/collect_staticmesh_type.py ================================================ # -*- coding: utf-8 -*- """Collector for staticMesh types. """ import pyblish.api class CollectStaticMeshType(pyblish.api.InstancePlugin): """Collect data type for fbx instance.""" hosts = ["houdini"] families = ["staticMesh"] label = "Collect type of staticMesh" order = pyblish.api.CollectorOrder def process(self, instance): if instance.data["creator_identifier"] == "io.openpype.creators.houdini.staticmesh.fbx": # noqa: E501 # Marking this instance as FBX triggers the FBX extractor. instance.data["families"] += ["fbx"] ================================================ FILE: openpype/hosts/houdini/plugins/publish/collect_usd_bootstrap.py ================================================ import pyblish.api from openpype.client import ( get_subset_by_name, get_asset_by_name, get_asset_name_identifier, ) import openpype.lib.usdlib as usdlib class CollectUsdBootstrap(pyblish.api.InstancePlugin): """Collect special Asset/Shot bootstrap instances if those are needed. Some specific subsets are intended to be part of the default structure of an "Asset" or "Shot" in our USD pipeline. For example, for an Asset we layer a Model and Shade USD file over each other and expose that in a Asset USD file, ready to use. On the first publish of any of the components of a Asset or Shot the missing pieces are bootstrapped and generated in the pipeline too. This means that on the very first publish of your model the Asset USD file will exist too. """ order = pyblish.api.CollectorOrder + 0.35 label = "Collect USD Bootstrap" hosts = ["houdini"] families = ["usd", "usd.layered"] def process(self, instance): # Detect whether the current subset is a subset in a pipeline def get_bootstrap(instance): instance_subset = instance.data["subset"] for name, layers in usdlib.PIPELINE.items(): if instance_subset in set(layers): return name # e.g. "asset" break else: return bootstrap = get_bootstrap(instance) if bootstrap: self.add_bootstrap(instance, bootstrap) # Check if any of the dependencies requires a bootstrap for dependency in instance.data.get("publishDependencies", list()): bootstrap = get_bootstrap(dependency) if bootstrap: self.add_bootstrap(dependency, bootstrap) def add_bootstrap(self, instance, bootstrap): self.log.debug("Add bootstrap for: %s" % bootstrap) project_name = instance.context.data["projectName"] asset_name = instance.data["asset"] asset_doc = get_asset_by_name(project_name, asset_name) assert asset_doc, "Asset must exist: %s" % asset_name # Check which are not about to be created and don't exist yet required = {"shot": ["usdShot"], "asset": ["usdAsset"]}.get(bootstrap) require_all_layers = instance.data.get("requireAllLayers", False) if require_all_layers: # USD files load fine in usdview and Houdini even when layered or # referenced files do not exist. So by default we don't require # the layers to exist. layers = usdlib.PIPELINE.get(bootstrap) if layers: required += list(layers) self.log.debug("Checking required bootstrap: %s" % required) for subset_name in required: if self._subset_exists( project_name, instance, subset_name, asset_doc ): continue self.log.debug( "Creating {0} USD bootstrap: {1} {2}".format( bootstrap, asset_name, subset_name ) ) new = instance.context.create_instance(subset_name) new.data["subset"] = subset_name new.data["label"] = "{0} ({1})".format(subset_name, asset_name) new.data["family"] = "usd.bootstrap" new.data["comment"] = "Automated bootstrap USD file." new.data["publishFamilies"] = ["usd"] # Do not allow the user to toggle this instance new.data["optional"] = False # Copy some data from the instance for which we bootstrap for key in ["asset"]: new.data[key] = instance.data[key] def _subset_exists(self, project_name, instance, subset_name, asset_doc): """Return whether subset exists in current context or in database.""" # Allow it to be created during this publish session context = instance.context asset_doc_name = get_asset_name_identifier(asset_doc) for inst in context: if ( inst.data["subset"] == subset_name and inst.data["asset"] == asset_doc_name ): return True # Or, if they already exist in the database we can # skip them too. if get_subset_by_name( project_name, subset_name, asset_doc["_id"], fields=["_id"] ): return True return False ================================================ FILE: openpype/hosts/houdini/plugins/publish/collect_usd_layers.py ================================================ import os import pyblish.api import openpype.hosts.houdini.api.usd as usdlib import hou class CollectUsdLayers(pyblish.api.InstancePlugin): """Collect the USD Layers that have configured save paths.""" order = pyblish.api.CollectorOrder + 0.35 label = "Collect USD Layers" hosts = ["houdini"] families = ["usd"] def process(self, instance): output = instance.data.get("output_node") if not output: self.log.debug("No output node found..") return rop_node = hou.node(instance.data["instance_node"]) save_layers = [] for layer in usdlib.get_configured_save_layers(rop_node): info = layer.rootPrims.get("HoudiniLayerInfo") save_path = info.customData.get("HoudiniSavePath") creator = info.customData.get("HoudiniCreatorNode") self.log.debug("Found configured save path: " "%s -> %s" % (layer, save_path)) # Log node that configured this save path if creator: self.log.debug("Created by: %s" % creator) save_layers.append((layer, save_path)) # Store on the instance instance.data["usdConfiguredSavePaths"] = save_layers # Create configured layer instances so User can disable updating # specific configured layers for publishing. context = instance.context for layer, save_path in save_layers: name = os.path.basename(save_path) label = "{0} -> {1}".format(instance.data["name"], name) layer_inst = context.create_instance(name) family = "usdlayer" layer_inst.data["family"] = family layer_inst.data["families"] = [family] layer_inst.data["subset"] = "__stub__" layer_inst.data["label"] = label layer_inst.data["asset"] = instance.data["asset"] layer_inst.data["instance_node"] = instance.data["instance_node"] # include same USD ROP layer_inst.append(rop_node) # include layer data layer_inst.append((layer, save_path)) # Allow this subset to be grouped into a USD Layer on creation layer_inst.data["subsetGroup"] = "USD Layer" ================================================ FILE: openpype/hosts/houdini/plugins/publish/collect_vray_rop.py ================================================ import re import os import hou import pyblish.api from openpype.hosts.houdini.api.lib import ( evalParmNoFrame, get_color_management_preferences ) from openpype.hosts.houdini.api import ( colorspace ) class CollectVrayROPRenderProducts(pyblish.api.InstancePlugin): """Collect Vray Render Products Collects the instance.data["files"] for the render products. Provides: instance -> files """ label = "VRay ROP Render Products" # This specific order value is used so that # this plugin runs after CollectFrames order = pyblish.api.CollectorOrder + 0.11 hosts = ["houdini"] families = ["vray_rop"] def process(self, instance): rop = hou.node(instance.data.get("instance_node")) # Collect chunkSize chunk_size_parm = rop.parm("chunkSize") if chunk_size_parm: chunk_size = int(chunk_size_parm.eval()) instance.data["chunkSize"] = chunk_size self.log.debug("Chunk Size: %s" % chunk_size) default_prefix = evalParmNoFrame(rop, "SettingsOutput_img_file_path") render_products = [] # TODO: add render elements if render element # Store whether we are splitting the render job in an export + render split_render = rop.parm("render_export_mode").eval() == "2" instance.data["splitRender"] = split_render export_prefix = None export_products = [] if split_render: export_prefix = evalParmNoFrame( rop, "render_export_filepath", pad_character="0" ) beauty_export_product = self.get_render_product_name( prefix=export_prefix, suffix=None) export_products.append(beauty_export_product) self.log.debug( "Found export product: {}".format(beauty_export_product) ) instance.data["ifdFile"] = beauty_export_product instance.data["exportFiles"] = list(export_products) beauty_product = self.get_render_product_name(default_prefix) render_products.append(beauty_product) files_by_aov = { "": self.generate_expected_files(instance, beauty_product)} if instance.data.get("RenderElement", True): render_element = self.get_render_element_name(rop, default_prefix) if render_element: for aov, renderpass in render_element.items(): render_products.append(renderpass) files_by_aov[aov] = self.generate_expected_files( instance, renderpass) for product in render_products: self.log.debug("Found render product: %s" % product) filenames = list(render_products) instance.data["files"] = filenames instance.data["renderProducts"] = colorspace.ARenderProduct() # For now by default do NOT try to publish the rendered output instance.data["publishJobState"] = "Suspended" instance.data["attachTo"] = [] # stub required data if "expectedFiles" not in instance.data: instance.data["expectedFiles"] = list() instance.data["expectedFiles"].append(files_by_aov) self.log.debug("expectedFiles:{}".format(files_by_aov)) # update the colorspace data colorspace_data = get_color_management_preferences() instance.data["colorspaceConfig"] = colorspace_data["config"] instance.data["colorspaceDisplay"] = colorspace_data["display"] instance.data["colorspaceView"] = colorspace_data["view"] def get_render_product_name(self, prefix, suffix=""): """Return the beauty output filename if render element enabled """ # Remove aov suffix from the product: `prefix.aov_suffix` -> `prefix` aov_parm = ".{}".format(suffix) return prefix.replace(aov_parm, "") def get_render_element_name(self, node, prefix, suffix=" "): """Return the output filename using the AOV prefix and suffix """ render_element_dict = {} # need a rewrite re_path = node.evalParm("render_network_render_channels") if re_path: node_children = hou.node(re_path).children() for element in node_children: if element.shaderName() != "vray:SettingsRenderChannels": aov = str(element) render_product = prefix.replace(suffix, aov) render_element_dict[aov] = render_product return render_element_dict def generate_expected_files(self, instance, path): """Create expected files in instance data""" dir = os.path.dirname(path) file = os.path.basename(path) if "#" in file: def replace(match): return "%0{}d".format(len(match.group())) file = re.sub("#+", replace, file) if "%" not in file: return path expected_files = [] start = instance.data["frameStartHandle"] end = instance.data["frameEndHandle"] for i in range(int(start), (int(end) + 1)): expected_files.append( os.path.join(dir, (file % i)).replace("\\", "/")) return expected_files ================================================ FILE: openpype/hosts/houdini/plugins/publish/collect_workfile.py ================================================ import os import pyblish.api class CollectWorkfile(pyblish.api.InstancePlugin): """Inject workfile representation into instance""" order = pyblish.api.CollectorOrder - 0.01 label = "Houdini Workfile Data" hosts = ["houdini"] families = ["workfile"] def process(self, instance): current_file = instance.context.data["currentFile"] folder, file = os.path.split(current_file) filename, ext = os.path.splitext(file) instance.data.update({ "setMembers": [current_file], "frameStart": instance.context.data['frameStart'], "frameEnd": instance.context.data['frameEnd'], "handleStart": instance.context.data['handleStart'], "handleEnd": instance.context.data['handleEnd'] }) instance.data['representations'] = [{ 'name': ext.lstrip("."), 'ext': ext.lstrip("."), 'files': file, "stagingDir": folder, }] self.log.debug('Collected workfile instance: {}'.format(file)) ================================================ FILE: openpype/hosts/houdini/plugins/publish/collect_workscene_fps.py ================================================ import pyblish.api import hou class CollectWorksceneFPS(pyblish.api.ContextPlugin): """Get the FPS of the work scene.""" label = "Workscene FPS" order = pyblish.api.CollectorOrder hosts = ["houdini"] def process(self, context): fps = hou.fps() self.log.info("Workscene FPS: %s" % fps) context.data.update({"fps": fps}) ================================================ FILE: openpype/hosts/houdini/plugins/publish/extract_alembic.py ================================================ import os import pyblish.api from openpype.pipeline import publish from openpype.hosts.houdini.api.lib import render_rop import hou class ExtractAlembic(publish.Extractor): order = pyblish.api.ExtractorOrder label = "Extract Alembic" hosts = ["houdini"] families = ["abc", "camera"] targets = ["local", "remote"] def process(self, instance): if instance.data.get("farm"): self.log.debug("Should be processed on farm, skipping.") return ropnode = hou.node(instance.data["instance_node"]) # Get the filename from the filename parameter output = ropnode.evalParm("filename") staging_dir = os.path.dirname(output) instance.data["stagingDir"] = staging_dir file_name = os.path.basename(output) # We run the render self.log.info("Writing alembic '%s' to '%s'" % (file_name, staging_dir)) render_rop(ropnode) if "representations" not in instance.data: instance.data["representations"] = [] representation = { 'name': 'abc', 'ext': 'abc', 'files': file_name, "stagingDir": staging_dir, } instance.data["representations"].append(representation) ================================================ FILE: openpype/hosts/houdini/plugins/publish/extract_ass.py ================================================ import os import pyblish.api from openpype.pipeline import publish from openpype.hosts.houdini.api.lib import render_rop import hou class ExtractAss(publish.Extractor): order = pyblish.api.ExtractorOrder + 0.1 label = "Extract Ass" families = ["ass"] hosts = ["houdini"] targets = ["local", "remote"] def process(self, instance): if instance.data.get("farm"): self.log.debug("Should be processed on farm, skipping.") return ropnode = hou.node(instance.data["instance_node"]) # Get the filename from the filename parameter # `.evalParm(parameter)` will make sure all tokens are resolved output = ropnode.evalParm("ar_ass_file") staging_dir = os.path.dirname(output) instance.data["stagingDir"] = staging_dir file_name = os.path.basename(output) # We run the render self.log.info("Writing ASS '%s' to '%s'" % (file_name, staging_dir)) render_rop(ropnode) # Unfortunately user interrupting the extraction does not raise an # error and thus still continues to the integrator. To capture that # we make sure all files exist files = instance.data["frames"] missing = [] for file_name in files: full_path = os.path.normpath(os.path.join(staging_dir, file_name)) if not os.path.exists(full_path): missing.append(full_path) if missing: raise RuntimeError("Failed to complete Arnold ass extraction. " "Missing output files: {}".format(missing)) if "representations" not in instance.data: instance.data["representations"] = [] # Allow ass.gz extension as well ext = "ass.gz" if file_name.endswith(".ass.gz") else "ass" representation = { 'name': 'ass', 'ext': ext, "files": files, "stagingDir": staging_dir, "frameStart": instance.data["frameStartHandle"], "frameEnd": instance.data["frameEndHandle"], } instance.data["representations"].append(representation) ================================================ FILE: openpype/hosts/houdini/plugins/publish/extract_bgeo.py ================================================ import os import pyblish.api from openpype.pipeline import publish from openpype.hosts.houdini.api.lib import render_rop from openpype.hosts.houdini.api import lib import hou class ExtractBGEO(publish.Extractor): order = pyblish.api.ExtractorOrder label = "Extract BGEO" hosts = ["houdini"] families = ["bgeo"] def process(self, instance): if instance.data.get("farm"): self.log.debug("Should be processed on farm, skipping.") return ropnode = hou.node(instance.data["instance_node"]) # Get the filename from the filename parameter output = ropnode.evalParm("sopoutput") staging_dir, file_name = os.path.split(output) instance.data["stagingDir"] = staging_dir # We run the render self.log.info("Writing bgeo files '{}' to '{}'.".format( file_name, staging_dir)) # write files render_rop(ropnode) output = instance.data["frames"] _, ext = lib.splitext( output[0], allowed_multidot_extensions=[ ".ass.gz", ".bgeo.sc", ".bgeo.gz", ".bgeo.lzma", ".bgeo.bz2"]) if "representations" not in instance.data: instance.data["representations"] = [] representation = { "name": "bgeo", "ext": ext.lstrip("."), "files": output, "stagingDir": staging_dir, "frameStart": instance.data["frameStartHandle"], "frameEnd": instance.data["frameEndHandle"] } instance.data["representations"].append(representation) ================================================ FILE: openpype/hosts/houdini/plugins/publish/extract_composite.py ================================================ import os import pyblish.api from openpype.pipeline import publish from openpype.hosts.houdini.api.lib import render_rop, splitext import hou class ExtractComposite(publish.Extractor): order = pyblish.api.ExtractorOrder label = "Extract Composite (Image Sequence)" hosts = ["houdini"] families = ["imagesequence"] def process(self, instance): ropnode = hou.node(instance.data["instance_node"]) # Get the filename from the copoutput parameter # `.evalParm(parameter)` will make sure all tokens are resolved output = ropnode.evalParm("copoutput") staging_dir = os.path.dirname(output) instance.data["stagingDir"] = staging_dir file_name = os.path.basename(output) self.log.info("Writing comp '%s' to '%s'" % (file_name, staging_dir)) render_rop(ropnode) output = instance.data["frames"] _, ext = splitext(output[0], []) ext = ext.lstrip(".") if "representations" not in instance.data: instance.data["representations"] = [] representation = { "name": ext, "ext": ext, "files": output, "stagingDir": staging_dir, "frameStart": instance.data["frameStartHandle"], "frameEnd": instance.data["frameEndHandle"], } from pprint import pformat self.log.info(pformat(representation)) instance.data["representations"].append(representation) ================================================ FILE: openpype/hosts/houdini/plugins/publish/extract_fbx.py ================================================ # -*- coding: utf-8 -*- """Fbx Extractor for houdini. """ import os import pyblish.api from openpype.pipeline import publish from openpype.hosts.houdini.api.lib import render_rop import hou class ExtractFBX(publish.Extractor): label = "Extract FBX" families = ["fbx"] hosts = ["houdini"] order = pyblish.api.ExtractorOrder + 0.1 def process(self, instance): # get rop node ropnode = hou.node(instance.data.get("instance_node")) output_file = ropnode.evalParm("sopoutput") # get staging_dir and file_name staging_dir = os.path.normpath(os.path.dirname(output_file)) file_name = os.path.basename(output_file) # render rop self.log.debug("Writing FBX '%s' to '%s'", file_name, staging_dir) render_rop(ropnode) # prepare representation representation = { "name": "fbx", "ext": "fbx", "files": file_name, "stagingDir": staging_dir } # A single frame may also be rendered without start/end frame. if "frameStartHandle" in instance.data and "frameEndHandle" in instance.data: # noqa representation["frameStart"] = instance.data["frameStartHandle"] representation["frameEnd"] = instance.data["frameEndHandle"] # set value type for 'representations' key to list if "representations" not in instance.data: instance.data["representations"] = [] # update instance data instance.data["stagingDir"] = staging_dir instance.data["representations"].append(representation) ================================================ FILE: openpype/hosts/houdini/plugins/publish/extract_hda.py ================================================ # -*- coding: utf-8 -*- import os from pprint import pformat import pyblish.api from openpype.pipeline import publish import hou class ExtractHDA(publish.Extractor): order = pyblish.api.ExtractorOrder label = "Extract HDA" hosts = ["houdini"] families = ["hda"] def process(self, instance): self.log.info(pformat(instance.data)) hda_node = hou.node(instance.data.get("instance_node")) hda_def = hda_node.type().definition() hda_options = hda_def.options() hda_options.setSaveInitialParmsAndContents(True) next_version = instance.data["anatomyData"]["version"] self.log.info("setting version: {}".format(next_version)) hda_def.setVersion(str(next_version)) hda_def.setOptions(hda_options) hda_def.save(hda_def.libraryFilePath(), hda_node, hda_options) if "representations" not in instance.data: instance.data["representations"] = [] file = os.path.basename(hda_def.libraryFilePath()) staging_dir = os.path.dirname(hda_def.libraryFilePath()) self.log.info("Using HDA from {}".format(hda_def.libraryFilePath())) representation = { 'name': 'hda', 'ext': 'hda', 'files': file, "stagingDir": staging_dir, } instance.data["representations"].append(representation) ================================================ FILE: openpype/hosts/houdini/plugins/publish/extract_mantra_ifd.py ================================================ import os import pyblish.api from openpype.pipeline import publish import hou class ExtractMantraIFD(publish.Extractor): order = pyblish.api.ExtractorOrder label = "Extract Mantra ifd" hosts = ["houdini"] families = ["mantraifd"] targets = ["local", "remote"] def process(self, instance): if instance.data.get("farm"): self.log.debug("Should be processed on farm, skipping.") return ropnode = hou.node(instance.data.get("instance_node")) output = ropnode.evalParm("soho_diskfile") staging_dir = os.path.dirname(output) instance.data["stagingDir"] = staging_dir files = instance.data["frames"] missing_frames = [ frame for frame in instance.data["frames"] if not os.path.exists( os.path.normpath(os.path.join(staging_dir, frame))) ] if missing_frames: raise RuntimeError("Failed to complete Mantra ifd extraction. " "Missing output files: {}".format( missing_frames)) if "representations" not in instance.data: instance.data["representations"] = [] representation = { 'name': 'ifd', 'ext': 'ifd', 'files': files, "stagingDir": staging_dir, "frameStart": instance.data["frameStart"], "frameEnd": instance.data["frameEnd"], } instance.data["representations"].append(representation) ================================================ FILE: openpype/hosts/houdini/plugins/publish/extract_opengl.py ================================================ import os import pyblish.api from openpype.pipeline import publish from openpype.hosts.houdini.api.lib import render_rop import hou class ExtractOpenGL(publish.Extractor): order = pyblish.api.ExtractorOrder - 0.01 label = "Extract OpenGL" families = ["review"] hosts = ["houdini"] def process(self, instance): ropnode = hou.node(instance.data.get("instance_node")) output = ropnode.evalParm("picture") staging_dir = os.path.normpath(os.path.dirname(output)) instance.data["stagingDir"] = staging_dir file_name = os.path.basename(output) self.log.info("Extracting '%s' to '%s'" % (file_name, staging_dir)) render_rop(ropnode) output = instance.data["frames"] tags = ["review"] if not instance.data.get("keepImages"): tags.append("delete") representation = { "name": instance.data["imageFormat"], "ext": instance.data["imageFormat"], "files": output, "stagingDir": staging_dir, "frameStart": instance.data["frameStartHandle"], "frameEnd": instance.data["frameEndHandle"], "tags": tags, "preview": True, "camera_name": instance.data.get("review_camera") } if "representations" not in instance.data: instance.data["representations"] = [] instance.data["representations"].append(representation) ================================================ FILE: openpype/hosts/houdini/plugins/publish/extract_redshift_proxy.py ================================================ import os import pyblish.api from openpype.pipeline import publish from openpype.hosts.houdini.api.lib import render_rop import hou class ExtractRedshiftProxy(publish.Extractor): order = pyblish.api.ExtractorOrder + 0.1 label = "Extract Redshift Proxy" families = ["redshiftproxy"] hosts = ["houdini"] targets = ["local", "remote"] def process(self, instance): if instance.data.get("farm"): self.log.debug("Should be processed on farm, skipping.") return ropnode = hou.node(instance.data.get("instance_node")) # Get the filename from the filename parameter # `.evalParm(parameter)` will make sure all tokens are resolved output = ropnode.evalParm("RS_archive_file") staging_dir = os.path.normpath(os.path.dirname(output)) instance.data["stagingDir"] = staging_dir file_name = os.path.basename(output) self.log.info("Writing Redshift Proxy '%s' to '%s'" % (file_name, staging_dir)) render_rop(ropnode) output = instance.data["frames"] if "representations" not in instance.data: instance.data["representations"] = [] representation = { "name": "rs", "ext": "rs", "files": output, "stagingDir": staging_dir, } # A single frame may also be rendered without start/end frame. if "frameStartHandle" in instance.data and "frameEndHandle" in instance.data: # noqa representation["frameStart"] = instance.data["frameStartHandle"] representation["frameEnd"] = instance.data["frameEndHandle"] instance.data["representations"].append(representation) ================================================ FILE: openpype/hosts/houdini/plugins/publish/extract_usd.py ================================================ import os import pyblish.api from openpype.pipeline import publish from openpype.hosts.houdini.api.lib import render_rop import hou class ExtractUSD(publish.Extractor): order = pyblish.api.ExtractorOrder label = "Extract USD" hosts = ["houdini"] families = ["usd", "usdModel", "usdSetDress"] def process(self, instance): ropnode = hou.node(instance.data.get("instance_node")) # Get the filename from the filename parameter output = ropnode.evalParm("lopoutput") staging_dir = os.path.dirname(output) instance.data["stagingDir"] = staging_dir file_name = os.path.basename(output) self.log.info("Writing USD '%s' to '%s'" % (file_name, staging_dir)) render_rop(ropnode) assert os.path.exists(output), "Output does not exist: %s" % output if "representations" not in instance.data: instance.data["representations"] = [] representation = { 'name': 'usd', 'ext': 'usd', 'files': file_name, "stagingDir": staging_dir, } instance.data["representations"].append(representation) ================================================ FILE: openpype/hosts/houdini/plugins/publish/extract_usd_layered.py ================================================ import os import contextlib import hou import sys from collections import deque import pyblish.api from openpype.client import ( get_asset_by_name, get_subset_by_name, get_last_version_by_subset_id, get_representation_by_name, ) from openpype.pipeline import ( get_representation_path, publish, ) import openpype.hosts.houdini.api.usd as hou_usdlib from openpype.hosts.houdini.api.lib import render_rop class ExitStack(object): """Context manager for dynamic management of a stack of exit callbacks. For example: with ExitStack() as stack: files = [stack.enter_context(open(fname)) for fname in filenames] # All opened files will automatically be closed at the end of # the with statement, even if attempts to open files later # in the list raise an exception """ def __init__(self): self._exit_callbacks = deque() def pop_all(self): """Preserve the context stack by transferring it to a new instance""" new_stack = type(self)() new_stack._exit_callbacks = self._exit_callbacks self._exit_callbacks = deque() return new_stack def _push_cm_exit(self, cm, cm_exit): """Helper to correctly register callbacks to __exit__ methods""" def _exit_wrapper(*exc_details): return cm_exit(cm, *exc_details) _exit_wrapper.__self__ = cm self.push(_exit_wrapper) def push(self, exit): """Registers a callback with the standard __exit__ method signature. Can suppress exceptions the same way __exit__ methods can. Also accepts any object with an __exit__ method (registering a call to the method instead of the object itself) """ # We use an unbound method rather than a bound method to follow # the standard lookup behaviour for special methods _cb_type = type(exit) try: exit_method = _cb_type.__exit__ except AttributeError: # Not a context manager, so assume its a callable self._exit_callbacks.append(exit) else: self._push_cm_exit(exit, exit_method) return exit # Allow use as a decorator def callback(self, callback, *args, **kwds): """Registers an arbitrary callback and arguments. Cannot suppress exceptions. """ def _exit_wrapper(exc_type, exc, tb): callback(*args, **kwds) # We changed the signature, so using @wraps is not appropriate, but # setting __wrapped__ may still help with introspection _exit_wrapper.__wrapped__ = callback self.push(_exit_wrapper) return callback # Allow use as a decorator def enter_context(self, cm): """Enters the supplied context manager If successful, also pushes its __exit__ method as a callback and returns the result of the __enter__ method. """ # We look up the special methods on the type to match the with # statement _cm_type = type(cm) _exit = _cm_type.__exit__ result = _cm_type.__enter__(cm) self._push_cm_exit(cm, _exit) return result def close(self): """Immediately unwind the context stack""" self.__exit__(None, None, None) def __enter__(self): return self def __exit__(self, *exc_details): # We manipulate the exception state so it behaves as though # we were actually nesting multiple with statements frame_exc = sys.exc_info()[1] def _fix_exception_context(new_exc, old_exc): while 1: exc_context = new_exc.__context__ if exc_context in (None, frame_exc): break new_exc = exc_context new_exc.__context__ = old_exc # Callbacks are invoked in LIFO order to match the behaviour of # nested context managers suppressed_exc = False while self._exit_callbacks: cb = self._exit_callbacks.pop() try: if cb(*exc_details): suppressed_exc = True exc_details = (None, None, None) except Exception: new_exc_details = sys.exc_info() # simulate the stack of exceptions by setting the context _fix_exception_context(new_exc_details[1], exc_details[1]) if not self._exit_callbacks: raise exc_details = new_exc_details return suppressed_exc @contextlib.contextmanager def parm_values(overrides): """Override Parameter values during the context.""" originals = [] try: for parm, value in overrides: originals.append((parm, parm.eval())) parm.set(value) yield finally: for parm, value in originals: # Parameter might not exist anymore so first # check whether it's still valid if hou.parm(parm.path()): parm.set(value) class ExtractUSDLayered(publish.Extractor): order = pyblish.api.ExtractorOrder label = "Extract Layered USD" hosts = ["houdini"] families = ["usdLayered", "usdShade"] # Force Output Processors so it will always save any file # into our unique staging directory with processed Avalon paths output_processors = ["avalon_uri_processor", "stagingdir_processor"] def process(self, instance): self.log.info("Extracting: %s" % instance) staging_dir = self.staging_dir(instance) fname = instance.data.get("usdFilename") # The individual rop nodes are collected as "publishDependencies" dependencies = instance.data["publishDependencies"] ropnodes = [dependency[0] for dependency in dependencies] assert all( node.type().name() in {"usd", "usd_rop"} for node in ropnodes ) # Main ROP node, either a USD Rop or ROP network with # multiple USD ROPs node = hou.node(instance.data["instance_node"]) # Collect any output dependencies that have not been processed yet # during extraction of other instances outputs = [fname] active_dependencies = [ dep for dep in dependencies if dep.data.get("publish", True) and not dep.data.get("_isExtracted", False) ] for dependency in active_dependencies: outputs.append(dependency.data["usdFilename"]) pattern = r"*[/\]{0} {0}" save_pattern = " ".join(pattern.format(fname) for fname in outputs) # Run a stack of context managers before we start the render to # temporarily adjust USD ROP settings for our publish output. rop_overrides = { # This sets staging directory on the processor to force our # output files to end up in the Staging Directory. "stagingdiroutputprocessor_stagingDir": staging_dir, # Force the Avalon URI Output Processor to refactor paths for # references, payloads and layers to published paths. "avalonurioutputprocessor_use_publish_paths": True, # Only write out specific USD files based on our outputs "savepattern": save_pattern, } overrides = list() with ExitStack() as stack: for ropnode in ropnodes: manager = hou_usdlib.outputprocessors( ropnode, processors=self.output_processors, disable_all_others=True, ) stack.enter_context(manager) # Some of these must be added after we enter the output # processor context manager because those parameters only # exist when the Output Processor is added to the ROP node. for name, value in rop_overrides.items(): parm = ropnode.parm(name) assert parm, "Parm not found: %s.%s" % ( ropnode.path(), name, ) overrides.append((parm, value)) stack.enter_context(parm_values(overrides)) # Render the single ROP node or the full ROP network render_rop(node) # Assert all output files in the Staging Directory for output_fname in outputs: path = os.path.join(staging_dir, output_fname) assert os.path.exists(path), "Output file must exist: %s" % path # Set up the dependency for publish if they have new content # compared to previous publishes project_name = instance.context.data["projectName"] for dependency in active_dependencies: dependency_fname = dependency.data["usdFilename"] filepath = os.path.join(staging_dir, dependency_fname) similar = self._compare_with_latest_publish( project_name, dependency, filepath ) if similar: # Deactivate this dependency self.log.debug( "Dependency matches previous publish version," " deactivating %s for publish" % dependency ) dependency.data["publish"] = False else: self.log.debug("Extracted dependency: %s" % dependency) # This dependency should be published dependency.data["files"] = [dependency_fname] dependency.data["stagingDir"] = staging_dir dependency.data["_isExtracted"] = True # Store the created files on the instance if "files" not in instance.data: instance.data["files"] = [] instance.data["files"].append(fname) def _compare_with_latest_publish(self, project_name, dependency, new_file): import filecmp _, ext = os.path.splitext(new_file) # Compare this dependency with the latest published version # to detect whether we should make this into a new publish # version. If not, skip it. asset = get_asset_by_name( project_name, dependency.data["asset"], fields=["_id"] ) subset = get_subset_by_name( project_name, dependency.data["subset"], asset["_id"], fields=["_id"] ) if not subset: # Subset doesn't exist yet. Definitely new file self.log.debug("No existing subset..") return False version = get_last_version_by_subset_id( project_name, subset["_id"], fields=["_id"] ) if not version: self.log.debug("No existing version..") return False representation = get_representation_by_name( project_name, ext.lstrip("."), version["_id"] ) if not representation: self.log.debug("No existing representation..") return False old_file = get_representation_path(representation) if not os.path.exists(old_file): return False return filecmp.cmp(old_file, new_file) ================================================ FILE: openpype/hosts/houdini/plugins/publish/extract_vdb_cache.py ================================================ import os import pyblish.api from openpype.pipeline import publish from openpype.hosts.houdini.api.lib import render_rop import hou class ExtractVDBCache(publish.Extractor): order = pyblish.api.ExtractorOrder + 0.1 label = "Extract VDB Cache" families = ["vdbcache"] hosts = ["houdini"] def process(self, instance): if instance.data.get("farm"): self.log.debug("Should be processed on farm, skipping.") return ropnode = hou.node(instance.data["instance_node"]) # Get the filename from the filename parameter # `.evalParm(parameter)` will make sure all tokens are resolved sop_output = ropnode.evalParm("sopoutput") staging_dir = os.path.normpath(os.path.dirname(sop_output)) instance.data["stagingDir"] = staging_dir file_name = os.path.basename(sop_output) self.log.info("Writing VDB '%s' to '%s'" % (file_name, staging_dir)) render_rop(ropnode) output = instance.data["frames"] if "representations" not in instance.data: instance.data["representations"] = [] representation = { "name": "vdb", "ext": "vdb", "files": output, "stagingDir": staging_dir, "frameStart": instance.data["frameStartHandle"], "frameEnd": instance.data["frameEndHandle"], } instance.data["representations"].append(representation) ================================================ FILE: openpype/hosts/houdini/plugins/publish/help/validate_vdb_output_node.xml ================================================ ================================================ FILE: openpype/hosts/houdini/plugins/publish/increment_current_file.py ================================================ import pyblish.api from openpype.lib import version_up from openpype.pipeline import registered_host from openpype.pipeline.publish import get_errored_plugins_from_context from openpype.hosts.houdini.api import HoudiniHost from openpype.pipeline.publish import KnownPublishError class IncrementCurrentFile(pyblish.api.ContextPlugin): """Increment the current file. Saves the current scene with an increased version number. """ label = "Increment current file" order = pyblish.api.IntegratorOrder + 9.0 hosts = ["houdini"] families = ["workfile", "redshift_rop", "arnold_rop", "mantra_rop", "karma_rop", "usdrender", "publish.hou"] optional = True def process(self, context): errored_plugins = get_errored_plugins_from_context(context) if any( plugin.__name__ == "HoudiniSubmitPublishDeadline" for plugin in errored_plugins ): raise KnownPublishError( "Skipping incrementing current file because " "submission to deadline failed." ) # Filename must not have changed since collecting host = registered_host() # type: HoudiniHost current_file = host.current_file() if context.data["currentFile"] != current_file: raise KnownPublishError( "Collected filename mismatches from current scene name." ) new_filepath = version_up(current_file) host.save_workfile(new_filepath) ================================================ FILE: openpype/hosts/houdini/plugins/publish/save_scene.py ================================================ import pyblish.api from openpype.pipeline import registered_host class SaveCurrentScene(pyblish.api.ContextPlugin): """Save current scene""" label = "Save current file" order = pyblish.api.ExtractorOrder - 0.49 hosts = ["houdini"] def process(self, context): # Filename must not have changed since collecting host = registered_host() current_file = host.get_current_workfile() assert context.data['currentFile'] == current_file, ( "Collected filename from current scene name." ) if host.workfile_has_unsaved_changes(): self.log.info("Saving current file: {}".format(current_file)) host.save_workfile(current_file) else: self.log.debug("No unsaved changes, skipping file save..") ================================================ FILE: openpype/hosts/houdini/plugins/publish/validate_abc_primitive_to_detail.py ================================================ # -*- coding: utf-8 -*- import pyblish.api from collections import defaultdict from openpype.pipeline import PublishValidationError class ValidateAbcPrimitiveToDetail(pyblish.api.InstancePlugin): """Validate Alembic ROP Primitive to Detail attribute is consistent. The Alembic ROP crashes Houdini whenever an attribute in the "Primitive to Detail" parameter exists on only a part of the primitives that belong to the same hierarchy path. Whenever it encounters inconsistent values, specifically where some are empty as opposed to others then Houdini crashes. (Tested in Houdini 17.5.229) """ order = pyblish.api.ValidatorOrder + 0.1 families = ["abc"] hosts = ["houdini"] label = "Validate Primitive to Detail (Abc)" def process(self, instance): invalid = self.get_invalid(instance) if invalid: raise PublishValidationError( ("Primitives found with inconsistent primitive " "to detail attributes. See log."), title=self.label ) @classmethod def get_invalid(cls, instance): import hou # noqa output_node = instance.data.get("output_node") rop_node = hou.node(instance.data["instance_node"]) if output_node is None: cls.log.error( "SOP Output node in '%s' does not exist. " "Ensure a valid SOP output path is set." % rop_node.path() ) return [rop_node.path()] pattern = rop_node.parm("prim_to_detail_pattern").eval().strip() if not pattern: cls.log.debug( "Alembic ROP has no 'Primitive to Detail' pattern. " "Validation is ignored.." ) return build_from_path = rop_node.parm("build_from_path").eval() if not build_from_path: cls.log.debug( "Alembic ROP has 'Build from Path' disabled. " "Validation is ignored.." ) return path_attr = rop_node.parm("path_attrib").eval() if not path_attr: cls.log.error( "The Alembic ROP node has no Path Attribute" "value set, but 'Build Hierarchy from Attribute'" "is enabled." ) return [rop_node.path()] # Let's assume each attribute is explicitly named for now and has no # wildcards for Primitive to Detail. This simplifies the check. cls.log.debug("Checking Primitive to Detail pattern: %s" % pattern) cls.log.debug("Checking with path attribute: %s" % path_attr) if not hasattr(output_node, "geometry"): # In the case someone has explicitly set an Object # node instead of a SOP node in Geometry context # then for now we ignore - this allows us to also # export object transforms. cls.log.warning("No geometry output node found, skipping check..") return # Check if the primitive attribute exists frame = instance.data.get("frameStart", 0) geo = output_node.geometryAtFrame(frame) # If there are no primitives on the start frame then it might be # something that is emitted over time. As such we can't actually # validate whether the attributes exist, because they won't exist # yet. In that case, just warn the user and allow it. if len(geo.iterPrims()) == 0: cls.log.warning( "No primitives found on current frame. Validation" " for Primitive to Detail will be skipped." ) return attrib = geo.findPrimAttrib(path_attr) if not attrib: cls.log.info( "Geometry Primitives are missing " "path attribute: `%s`" % path_attr ) return [output_node.path()] # Ensure at least a single string value is present if not attrib.strings(): cls.log.info( "Primitive path attribute has no " "string values: %s" % path_attr ) return [output_node.path()] paths = None for attr in pattern.split(" "): if not attr.strip(): # Ignore empty values continue # Check if the primitive attribute exists attrib = geo.findPrimAttrib(attr) if not attrib: # It is allowed to not have the attribute at all continue # The issue can only happen if at least one string attribute is # present. So we ignore cases with no values whatsoever. if not attrib.strings(): continue check = defaultdict(set) values = geo.primStringAttribValues(attr) if paths is None: paths = geo.primStringAttribValues(path_attr) for path, value in zip(paths, values): check[path].add(value) for path, values in check.items(): # Whenever a single path has multiple values for the # Primitive to Detail attribute then we consider it # inconsistent and invalidate the ROP node's content. if len(values) > 1: cls.log.warning( "Path has multiple values: %s (path: %s)" % (list(values), path) ) return [output_node.path()] ================================================ FILE: openpype/hosts/houdini/plugins/publish/validate_alembic_face_sets.py ================================================ # -*- coding: utf-8 -*- import pyblish.api import hou class ValidateAlembicROPFaceSets(pyblish.api.InstancePlugin): """Validate Face Sets are disabled for extraction to pointcache. When groups are saved as Face Sets with the Alembic these show up as shadingEngine connections in Maya - however, with animated groups these connections in Maya won't work as expected, it won't update per frame. Additionally, it can break shader assignments in some cases where it requires to first break this connection to allow a shader to be assigned. It is allowed to include Face Sets, so only an issue is logged to identify that it could introduce issues down the pipeline. """ order = pyblish.api.ValidatorOrder + 0.1 families = ["abc"] hosts = ["houdini"] label = "Validate Alembic ROP Face Sets" def process(self, instance): rop = hou.node(instance.data["instance_node"]) facesets = rop.parm("facesets").eval() # 0 = No Face Sets # 1 = Save Non-Empty Groups as Face Sets # 2 = Save All Groups As Face Sets if facesets != 0: self.log.warning( "Alembic ROP saves 'Face Sets' for Geometry. " "Are you sure you want this?" ) ================================================ FILE: openpype/hosts/houdini/plugins/publish/validate_alembic_input_node.py ================================================ # -*- coding: utf-8 -*- import pyblish.api from openpype.pipeline import PublishValidationError import hou class ValidateAlembicInputNode(pyblish.api.InstancePlugin): """Validate that the node connected to the output is correct. The connected node cannot be of the following types for Alembic: - VDB - Volume """ order = pyblish.api.ValidatorOrder + 0.1 families = ["abc"] hosts = ["houdini"] label = "Validate Input Node (Abc)" def process(self, instance): invalid = self.get_invalid(instance) if invalid: raise PublishValidationError( ("Primitive types found that are not supported " "for Alembic output."), title=self.label ) @classmethod def get_invalid(cls, instance): invalid_prim_types = ["VDB", "Volume"] output_node = instance.data.get("output_node") if output_node is None: node = hou.node(instance.data["instance_node"]) cls.log.error( "SOP Output node in '%s' does not exist. " "Ensure a valid SOP output path is set." % node.path() ) return [node.path()] if not hasattr(output_node, "geometry"): # In the case someone has explicitly set an Object # node instead of a SOP node in Geometry context # then for now we ignore - this allows us to also # export object transforms. cls.log.warning("No geometry output node found, skipping check..") return frame = instance.data.get("frameStart", 0) geo = output_node.geometryAtFrame(frame) invalid = False for prim_type in invalid_prim_types: if geo.countPrimType(prim_type) > 0: cls.log.error( "Found a primitive which is of type '%s' !" % prim_type ) invalid = True if invalid: return [instance] ================================================ FILE: openpype/hosts/houdini/plugins/publish/validate_animation_settings.py ================================================ import pyblish.api from openpype.pipeline.publish import PublishValidationError from openpype.hosts.houdini.api import lib import hou class ValidateAnimationSettings(pyblish.api.InstancePlugin): """Validate if the unexpanded string contains the frame ('$F') token This validator will only check the output parameter of the node if the Valid Frame Range is not set to 'Render Current Frame' Rules: If you render out a frame range it is mandatory to have the frame token - '$F4' or similar - to ensure that each frame gets written. If this is not the case you will override the same file every time a frame is written out. Examples: Good: 'my_vbd_cache.$F4.vdb' Bad: 'my_vbd_cache.vdb' """ order = pyblish.api.ValidatorOrder label = "Validate Frame Settings" families = ["vdbcache"] def process(self, instance): invalid = self.get_invalid(instance) if invalid: raise PublishValidationError( "Output settings do no match for '%s'" % instance ) @classmethod def get_invalid(cls, instance): node = hou.node(instance.data["instance_node"]) # Check trange parm, 0 means Render Current Frame frame_range = node.evalParm("trange") if frame_range == 0: return [] output_parm = lib.get_output_parameter(node) unexpanded_str = output_parm.unexpandedString() if "$F" not in unexpanded_str: cls.log.error("No frame token found in '%s'" % node.path()) return [instance] ================================================ FILE: openpype/hosts/houdini/plugins/publish/validate_bypass.py ================================================ # -*- coding: utf-8 -*- import pyblish.api from openpype.pipeline import PublishValidationError import hou class ValidateBypassed(pyblish.api.InstancePlugin): """Validate all primitives build hierarchy from attribute when enabled. The name of the attribute must exist on the prims and have the same name as Build Hierarchy from Attribute's `Path Attribute` value on the Alembic ROP node whenever Build Hierarchy from Attribute is enabled. """ order = pyblish.api.ValidatorOrder - 0.1 families = ["*"] hosts = ["houdini"] label = "Validate ROP Bypass" def process(self, instance): if len(instance) == 0: # Ignore instances without any nodes # e.g. in memory bootstrap instances return invalid = self.get_invalid(instance) if invalid: rop = invalid[0] raise PublishValidationError( ("ROP node {} is set to bypass, publishing cannot " "continue.".format(rop.path())), title=self.label ) @classmethod def get_invalid(cls, instance): rop = hou.node(instance.data["instance_node"]) if hasattr(rop, "isBypassed") and rop.isBypassed(): return [rop] ================================================ FILE: openpype/hosts/houdini/plugins/publish/validate_camera_rop.py ================================================ # -*- coding: utf-8 -*- """Validator plugin for Houdini Camera ROP settings.""" import pyblish.api from openpype.pipeline import PublishValidationError class ValidateCameraROP(pyblish.api.InstancePlugin): """Validate Camera ROP settings.""" order = pyblish.api.ValidatorOrder families = ["camera"] hosts = ["houdini"] label = "Camera ROP" def process(self, instance): import hou node = hou.node(instance.data.get("instance_node")) if node.parm("use_sop_path").eval(): raise PublishValidationError( ("Alembic ROP for Camera export should not be " "set to 'Use Sop Path'. Please disable."), title=self.label ) # Get the root and objects parameter of the Alembic ROP node root = node.parm("root").eval() objects = node.parm("objects").eval() errors = [] if not root: errors.append("Root parameter must be set on Alembic ROP") if not root.startswith("/"): errors.append("Root parameter must start with slash /") if not objects: errors.append("Objects parameter must be set on Alembic ROP") if len(objects.split(" ")) != 1: errors.append("Must have only a single object.") if errors: for error in errors: self.log.error(error) raise PublishValidationError( "Some checks failed, see validator log.", title=self.label) # Check if the object exists and is a camera path = root + "/" + objects camera = hou.node(path) if not camera: raise PublishValidationError( "Camera path does not exist: %s" % path, title=self.label) if camera.type().name() != "cam": raise PublishValidationError( ("Object set in Alembic ROP is not a camera: " "{} (type: {})").format(camera, camera.type().name()), title=self.label) ================================================ FILE: openpype/hosts/houdini/plugins/publish/validate_cop_output_node.py ================================================ # -*- coding: utf-8 -*- import sys import pyblish.api import six from openpype.pipeline import PublishValidationError class ValidateCopOutputNode(pyblish.api.InstancePlugin): """Validate the instance COP Output Node. This will ensure: - The COP Path is set. - The COP Path refers to an existing object. - The COP Path node is a COP node. """ order = pyblish.api.ValidatorOrder families = ["imagesequence"] hosts = ["houdini"] label = "Validate COP Output Node" def process(self, instance): invalid = self.get_invalid(instance) if invalid: raise PublishValidationError( ("Output node(s) `{}` are incorrect. " "See plug-in log for details.").format(invalid), title=self.label ) @classmethod def get_invalid(cls, instance): import hou try: output_node = instance.data["output_node"] except KeyError: six.reraise( PublishValidationError, PublishValidationError( "Can't determine COP output node.", title=cls.__name__), sys.exc_info()[2] ) if output_node is None: node = hou.node(instance.data.get("instance_node")) cls.log.error( "COP Output node in '%s' does not exist. " "Ensure a valid COP output path is set." % node.path() ) return [node.path()] # Output node must be a Sop node. if not isinstance(output_node, hou.CopNode): cls.log.error( "Output node %s is not a COP node. " "COP Path must point to a COP node, " "instead found category type: %s" % (output_node.path(), output_node.type().category().name()) ) return [output_node.path()] # For the sake of completeness also assert the category type # is Cop2 to avoid potential edge case scenarios even though # the isinstance check above should be stricter than this category if output_node.type().category().name() != "Cop2": raise PublishValidationError( ("Output node %s is not of category Cop2. " "This is a bug...").format(output_node.path()), title=cls.label) ================================================ FILE: openpype/hosts/houdini/plugins/publish/validate_fbx_output_node.py ================================================ # -*- coding: utf-8 -*- import pyblish.api from openpype.pipeline import PublishValidationError from openpype.hosts.houdini.api.action import ( SelectInvalidAction, SelectROPAction, ) from openpype.hosts.houdini.api.lib import get_obj_node_output import hou class ValidateFBXOutputNode(pyblish.api.InstancePlugin): """Validate the instance Output Node. This will ensure: - The Output Node Path is set. - The Output Node Path refers to an existing object. - The Output Node is a Sop or Obj node. - The Output Node has geometry data. - The Output Node doesn't include invalid primitive types. """ order = pyblish.api.ValidatorOrder families = ["fbx"] hosts = ["houdini"] label = "Validate FBX Output Node" actions = [SelectROPAction, SelectInvalidAction] def process(self, instance): invalid = self.get_invalid(instance) if invalid: nodes = [n.path() for n in invalid] raise PublishValidationError( "See log for details. " "Invalid nodes: {0}".format(nodes), title="Invalid output node(s)" ) @classmethod def get_invalid(cls, instance): output_node = instance.data.get("output_node") # Check if The Output Node Path is set and # refers to an existing object. if output_node is None: rop_node = hou.node(instance.data["instance_node"]) cls.log.error( "Output node in '%s' does not exist. " "Ensure a valid output path is set.", rop_node.path() ) return [rop_node] # Check if the Output Node is a Sop or an Obj node # also, list all sop output nodes inside as well as # invalid empty nodes. all_out_sops = [] invalid = [] # if output_node is an ObjSubnet or an ObjNetwork if output_node.childTypeCategory() == hou.objNodeTypeCategory(): for node in output_node.allSubChildren(): if node.type().name() == "geo": out = get_obj_node_output(node) if out: all_out_sops.append(out) else: invalid.append(node) # empty_objs cls.log.error( "Geo Obj Node '%s' is empty!", node.path() ) if not all_out_sops: invalid.append(output_node) # empty_objs cls.log.error( "Output Node '%s' is empty!", node.path() ) # elif output_node is an ObjNode elif output_node.type().name() == "geo": out = get_obj_node_output(output_node) if out: all_out_sops.append(out) else: invalid.append(node) # empty_objs cls.log.error( "Output Node '%s' is empty!", node.path() ) # elif output_node is a SopNode elif output_node.type().category().name() == "Sop": all_out_sops.append(output_node) # Then it's a wrong node type else: cls.log.error( "Output node %s is not a SOP or OBJ Geo or OBJ SubNet node. " "Instead found category type: %s %s", output_node.path(), output_node.type().category().name(), output_node.type().name() ) return [output_node] # Check if all output sop nodes have geometry # and don't contain invalid prims invalid_prim_types = ["VDB", "Volume"] for sop_node in all_out_sops: # Empty Geometry test if not hasattr(sop_node, "geometry"): invalid.append(sop_node) # empty_geometry cls.log.error( "Sop node '%s' doesn't include any prims.", sop_node.path() ) continue frame = instance.data.get("frameStart", 0) geo = sop_node.geometryAtFrame(frame) if len(geo.iterPrims()) == 0: invalid.append(sop_node) # empty_geometry cls.log.error( "Sop node '%s' doesn't include any prims.", sop_node.path() ) continue # Invalid Prims test for prim_type in invalid_prim_types: if geo.countPrimType(prim_type) > 0: invalid.append(sop_node) # invalid_prims cls.log.error( "Sop node '%s' includes invalid prims of type '%s'.", sop_node.path(), prim_type ) if invalid: return invalid ================================================ FILE: openpype/hosts/houdini/plugins/publish/validate_file_extension.py ================================================ # -*- coding: utf-8 -*- import os import pyblish.api from openpype.hosts.houdini.api import lib from openpype.pipeline import PublishValidationError import hou class ValidateFileExtension(pyblish.api.InstancePlugin): """Validate the output file extension fits the output family. File extensions: - Pointcache must be .abc - Camera must be .abc - VDB must be .vdb """ order = pyblish.api.ValidatorOrder families = ["camera", "vdbcache"] hosts = ["houdini"] label = "Output File Extension" family_extensions = { "camera": ".abc", "vdbcache": ".vdb", } def process(self, instance): invalid = self.get_invalid(instance) if invalid: raise PublishValidationError( "ROP node has incorrect file extension: {}".format(invalid), title=self.label ) @classmethod def get_invalid(cls, instance): # Get ROP node from instance node = hou.node(instance.data["instance_node"]) # Create lookup for current family in instance families = [] family = instance.data.get("family", None) if family: families.append(family) families = set(families) # Perform extension check output = lib.get_output_parameter(node).eval() _, output_extension = os.path.splitext(output) for family in families: extension = cls.family_extensions.get(family, None) if extension is None: raise PublishValidationError( "Unsupported family: {}".format(family), title=cls.label) if output_extension != extension: return [node.path()] ================================================ FILE: openpype/hosts/houdini/plugins/publish/validate_frame_range.py ================================================ # -*- coding: utf-8 -*- import pyblish.api from openpype.pipeline import PublishValidationError from openpype.pipeline.publish import RepairAction from openpype.hosts.houdini.api.action import SelectInvalidAction import hou class DisableUseAssetHandlesAction(RepairAction): label = "Disable use asset handles" icon = "mdi.toggle-switch-off" class ValidateFrameRange(pyblish.api.InstancePlugin): """Validate Frame Range. Due to the usage of start and end handles, then Frame Range must be >= (start handle + end handle) which results that frameEnd be smaller than frameStart """ order = pyblish.api.ValidatorOrder - 0.1 hosts = ["houdini"] label = "Validate Frame Range" actions = [DisableUseAssetHandlesAction, SelectInvalidAction] def process(self, instance): invalid = self.get_invalid(instance) if invalid: raise PublishValidationError( title="Invalid Frame Range", message=( "Invalid frame range because the instance " "start frame ({0[frameStart]}) is higher than " "the end frame ({0[frameEnd]})" .format(instance.data) ), description=( "## Invalid Frame Range\n" "The frame range for the instance is invalid because " "the start frame is higher than the end frame.\n\nThis " "is likely due to asset handles being applied to your " "instance or the ROP node's start frame " "is set higher than the end frame.\n\nIf your ROP frame " "range is correct and you do not want to apply asset " "handles make sure to disable Use asset handles on the " "publish instance." ) ) @classmethod def get_invalid(cls, instance): if not instance.data.get("instance_node"): return rop_node = hou.node(instance.data["instance_node"]) frame_start = instance.data.get("frameStart") frame_end = instance.data.get("frameEnd") if frame_start is None or frame_end is None: cls.log.debug( "Skipping frame range validation for " "instance without frame data: {}".format(rop_node.path()) ) return if frame_start > frame_end: cls.log.info( "The ROP node render range is set to " "{0[frameStartHandle]} - {0[frameEndHandle]} " "The asset handles applied to the instance are start handle " "{0[handleStart]} and end handle {0[handleEnd]}" .format(instance.data) ) return [rop_node] @classmethod def repair(cls, instance): if not cls.get_invalid(instance): # Already fixed return # Disable use asset handles context = instance.context create_context = context.data["create_context"] instance_id = instance.data.get("instance_id") if not instance_id: cls.log.debug("'{}' must have instance id" .format(instance)) return created_instance = create_context.get_instance_by_id(instance_id) if not instance_id: cls.log.debug("Unable to find instance '{}' by id" .format(instance)) return created_instance.publish_attributes["CollectAssetHandles"]["use_handles"] = False # noqa create_context.save_changes() cls.log.debug("use asset handles is turned off for '{}'" .format(instance)) ================================================ FILE: openpype/hosts/houdini/plugins/publish/validate_frame_token.py ================================================ import pyblish.api from openpype.hosts.houdini.api import lib import hou class ValidateFrameToken(pyblish.api.InstancePlugin): """Validate if the unexpanded string contains the frame ('$F') token. This validator will *only* check the output parameter of the node if the Valid Frame Range is not set to 'Render Current Frame' Rules: If you render out a frame range it is mandatory to have the frame token - '$F4' or similar - to ensure that each frame gets written. If this is not the case you will override the same file every time a frame is written out. Examples: Good: 'my_vbd_cache.$F4.vdb' Bad: 'my_vbd_cache.vdb' """ order = pyblish.api.ValidatorOrder label = "Validate Frame Token" families = ["vdbcache"] def process(self, instance): invalid = self.get_invalid(instance) if invalid: raise RuntimeError( "Output settings do no match for '%s'" % instance ) @classmethod def get_invalid(cls, instance): node = hou.node(instance.data["instance_node"]) # Check trange parm, 0 means Render Current Frame frame_range = node.evalParm("trange") if frame_range == 0: return [] output_parm = lib.get_output_parameter(node) unexpanded_str = output_parm.unexpandedString() if "$F" not in unexpanded_str: cls.log.error("No frame token found in '%s'" % node.path()) return [instance] ================================================ FILE: openpype/hosts/houdini/plugins/publish/validate_houdini_license_category.py ================================================ # -*- coding: utf-8 -*- import pyblish.api from openpype.pipeline import PublishValidationError import hou class ValidateHoudiniNotApprenticeLicense(pyblish.api.InstancePlugin): """Validate the Houdini instance runs a non Apprentice license. USD ROPs: When extracting USD files from an apprentice Houdini license, the resulting files will get "scrambled" with a license protection and get a special .usdnc suffix. This currently breaks the Subset/representation pipeline so we disallow any publish with apprentice license. Alembic ROPs: Houdini Apprentice does not export Alembic. """ order = pyblish.api.ValidatorOrder families = ["usd", "abc", "fbx", "camera"] hosts = ["houdini"] label = "Houdini Apprentice License" def process(self, instance): if hou.isApprentice(): # Find which family was matched with the plug-in families = {instance.data["family"]} families.update(instance.data.get("families", [])) disallowed_families = families.intersection(self.families) families = " ".join(sorted(disallowed_families)).title() raise PublishValidationError( "{} publishing requires a non apprentice license." .format(families), title=self.label) ================================================ FILE: openpype/hosts/houdini/plugins/publish/validate_mesh_is_static.py ================================================ # -*- coding: utf-8 -*- """Validator for correct naming of Static Meshes.""" import pyblish.api from openpype.pipeline import ( PublishValidationError, OptionalPyblishPluginMixin ) from openpype.pipeline.publish import ValidateContentsOrder from openpype.hosts.houdini.api.action import SelectInvalidAction from openpype.hosts.houdini.api.lib import get_output_children class ValidateMeshIsStatic(pyblish.api.InstancePlugin, OptionalPyblishPluginMixin): """Validate mesh is static. It checks if output node is time dependent. """ families = ["staticMesh"] hosts = ["houdini"] label = "Validate Mesh is Static" order = ValidateContentsOrder + 0.1 actions = [SelectInvalidAction] def process(self, instance): invalid = self.get_invalid(instance) if invalid: nodes = [n.path() for n in invalid] raise PublishValidationError( "See log for details. " "Invalid nodes: {0}".format(nodes) ) @classmethod def get_invalid(cls, instance): invalid = [] output_node = instance.data.get("output_node") if output_node is None: cls.log.debug( "No Output Node, skipping check.." ) return all_outputs = get_output_children(output_node) for output in all_outputs: if output.isTimeDependent(): invalid.append(output) cls.log.error( "Output node '%s' is time dependent.", output.path() ) return invalid ================================================ FILE: openpype/hosts/houdini/plugins/publish/validate_mkpaths_toggled.py ================================================ # -*- coding: utf-8 -*- import pyblish.api from openpype.pipeline import PublishValidationError class ValidateIntermediateDirectoriesChecked(pyblish.api.InstancePlugin): """Validate Create Intermediate Directories is enabled on ROP node.""" order = pyblish.api.ValidatorOrder families = ["pointcache", "camera", "vdbcache"] hosts = ["houdini"] label = "Create Intermediate Directories Checked" def process(self, instance): invalid = self.get_invalid(instance) if invalid: raise PublishValidationError( ("Found ROP node with Create Intermediate " "Directories turned off: {}".format(invalid)), title=self.label) @classmethod def get_invalid(cls, instance): result = [] for node in instance[:]: if node.parm("mkpath").eval() != 1: cls.log.error("Invalid settings found on `%s`" % node.path()) result.append(node.path()) return result ================================================ FILE: openpype/hosts/houdini/plugins/publish/validate_no_errors.py ================================================ # -*- coding: utf-8 -*- import pyblish.api import hou from openpype.pipeline import PublishValidationError def cook_in_range(node, start, end): current = hou.intFrame() if start >= current >= end: # Allow cooking current frame since we're in frame range node.cook(force=False) else: node.cook(force=False, frame_range=(start, start)) def get_errors(node): """Get cooking errors. If node already has errors check whether it needs to recook If so, then recook first to see if that solves it. """ if node.errors() and node.needsToCook(): node.cook() return node.errors() class ValidateNoErrors(pyblish.api.InstancePlugin): """Validate the Instance has no current cooking errors.""" order = pyblish.api.ValidatorOrder hosts = ["houdini"] label = "Validate no errors" def process(self, instance): validate_nodes = [] if len(instance) > 0: validate_nodes.append(hou.node(instance.data.get("instance_node"))) output_node = instance.data.get("output_node") if output_node: validate_nodes.append(output_node) for node in validate_nodes: self.log.debug("Validating for errors: %s" % node.path()) errors = get_errors(node) if errors: # If there are current errors, then try an unforced cook # to see whether the error will disappear. self.log.debug( "Recooking to revalidate error " "is up to date for: %s" % node.path() ) current_frame = hou.intFrame() start = instance.data.get("frameStart", current_frame) end = instance.data.get("frameEnd", current_frame) cook_in_range(node, start=start, end=end) # Check for errors again after the forced recook errors = get_errors(node) if errors: self.log.error(errors) raise PublishValidationError( "Node has errors: {}".format(node.path()), title=self.label) ================================================ FILE: openpype/hosts/houdini/plugins/publish/validate_primitive_hierarchy_paths.py ================================================ # -*- coding: utf-8 -*- import pyblish.api from openpype.pipeline import PublishValidationError from openpype.pipeline.publish import ( ValidateContentsOrder, RepairAction, ) import hou class AddDefaultPathAction(RepairAction): label = "Add a default path attribute" icon = "mdi.pencil-plus-outline" class ValidatePrimitiveHierarchyPaths(pyblish.api.InstancePlugin): """Validate all primitives build hierarchy from attribute when enabled. The name of the attribute must exist on the prims and have the same name as Build Hierarchy from Attribute's `Path Attribute` value on the Alembic ROP node whenever Build Hierarchy from Attribute is enabled. """ order = ValidateContentsOrder + 0.1 families = ["abc"] hosts = ["houdini"] label = "Validate Prims Hierarchy Path" actions = [AddDefaultPathAction] def process(self, instance): invalid = self.get_invalid(instance) if invalid: nodes = [n.path() for n in invalid] raise PublishValidationError( "See log for details. " "Invalid nodes: {0}".format(nodes), title=self.label ) @classmethod def get_invalid(cls, instance): output_node = instance.data.get("output_node") rop_node = hou.node(instance.data["instance_node"]) if output_node is None: cls.log.error( "SOP Output node in '%s' does not exist. " "Ensure a valid SOP output path is set.", rop_node.path() ) return [rop_node] build_from_path = rop_node.parm("build_from_path").eval() if not build_from_path: cls.log.debug( "Alembic ROP has 'Build from Path' disabled. " "Validation is ignored.." ) return path_attr = rop_node.parm("path_attrib").eval() if not path_attr: cls.log.error( "The Alembic ROP node has no Path Attribute" "value set, but 'Build Hierarchy from Attribute'" "is enabled." ) return [rop_node] cls.log.debug("Checking for attribute: %s", path_attr) if not hasattr(output_node, "geometry"): # In the case someone has explicitly set an Object # node instead of a SOP node in Geometry context # then for now we ignore - this allows us to also # export object transforms. cls.log.warning("No geometry output node found, skipping check..") return # Check if the primitive attribute exists frame = instance.data.get("frameStart", 0) geo = output_node.geometryAtFrame(frame) # If there are no primitives on the current frame then we can't # check whether the path names are correct. So we'll just issue a # warning that the check can't be done consistently and skip # validation. if len(geo.iterPrims()) == 0: cls.log.warning( "No primitives found on current frame. Validation" " for primitive hierarchy paths will be skipped," " thus can't be validated." ) return # Check if there are any values for the primitives attrib = geo.findPrimAttrib(path_attr) if not attrib: cls.log.info( "Geometry Primitives are missing " "path attribute: `%s`", path_attr ) return [output_node] # Ensure at least a single string value is present if not attrib.strings(): cls.log.info( "Primitive path attribute has no " "string values: %s", path_attr ) return [output_node] paths = geo.primStringAttribValues(path_attr) # Ensure all primitives are set to a valid path # Collect all invalid primitive numbers invalid_prims = [i for i, path in enumerate(paths) if not path] if invalid_prims: num_prims = len(geo.iterPrims()) # faster than len(geo.prims()) cls.log.info( "Prims have no value for attribute `%s` " "(%s of %s prims)", path_attr, len(invalid_prims), num_prims ) return [output_node] @classmethod def repair(cls, instance): """Add a default path attribute Action. It is a helper action more than a repair action, used to add a default single value for the path. """ rop_node = hou.node(instance.data["instance_node"]) output_node = rop_node.parm("sop_path").evalAsNode() if not output_node: cls.log.debug( "Action isn't performed, invalid SOP Path on %s", rop_node ) return # This check to prevent the action from running multiple times. # git_invalid only returns [output_node] when # path attribute is the problem if cls.get_invalid(instance) != [output_node]: return path_attr = rop_node.parm("path_attrib").eval() path_node = output_node.parent().createNode("name", "AUTO_PATH") path_node.parm("attribname").set(path_attr) path_node.parm("name1").set('`opname("..")`/`opname("..")`Shape') cls.log.debug( "'%s' was created. It adds '%s' with a default single value", path_node, path_attr ) path_node.setGenericFlag(hou.nodeFlag.DisplayComment, True) path_node.setComment( 'Auto path node was created automatically by ' '"Add a default path attribute"' '\nFeel free to modify or replace it.' ) if output_node.type().name() in ["null", "output"]: # Connect before path_node.setFirstInput(output_node.input(0)) path_node.moveToGoodPosition() output_node.setFirstInput(path_node) output_node.moveToGoodPosition() else: # Connect after path_node.setFirstInput(output_node) rop_node.parm("sop_path").set(path_node.path()) path_node.moveToGoodPosition() cls.log.debug( "SOP path on '%s' updated to new output node '%s'", rop_node, path_node ) ================================================ FILE: openpype/hosts/houdini/plugins/publish/validate_remote_publish.py ================================================ # -*-coding: utf-8 -*- import pyblish.api from openpype.hosts.houdini.api import lib from openpype.pipeline.publish import RepairContextAction from openpype.pipeline import PublishValidationError import hou class ValidateRemotePublishOutNode(pyblish.api.ContextPlugin): """Validate the remote publish out node exists for Deadline to trigger.""" order = pyblish.api.ValidatorOrder - 0.4 families = ["*"] hosts = ["houdini"] targets = ["deadline"] label = "Remote Publish ROP node" actions = [RepairContextAction] def process(self, context): cmd = "import colorbleed.lib; colorbleed.lib.publish_remote()" node = hou.node("/out/REMOTE_PUBLISH") if not node: raise RuntimeError("Missing REMOTE_PUBLISH node.") # We ensure it's a shell node and that it has the pre-render script # set correctly. Plus the shell script it will trigger should be # completely empty (doing nothing) if node.type().name() != "shell": self.raise_error("Must be shell ROP node") if node.parm("command").eval() != "": self.raise_error("Must have no command") if node.parm("shellexec").eval(): self.raise_error("Must not execute in shell") if node.parm("prerender").eval() != cmd: self.raise_error("REMOTE_PUBLISH node does not have " "correct prerender script.") if node.parm("lprerender").eval() != "python": self.raise_error("REMOTE_PUBLISH node prerender script " "type not set to 'python'") @classmethod def repair(cls, context): """(Re)create the node if it fails to pass validation.""" lib.create_remote_publish_node(force=True) def raise_error(self, message): raise PublishValidationError(message) ================================================ FILE: openpype/hosts/houdini/plugins/publish/validate_remote_publish_enabled.py ================================================ # -*- coding: utf-8 -*- import pyblish.api import hou from openpype.pipeline.publish import RepairContextAction from openpype.pipeline import PublishValidationError class ValidateRemotePublishEnabled(pyblish.api.ContextPlugin): """Validate the remote publish node is *not* bypassed.""" order = pyblish.api.ValidatorOrder - 0.39 families = ["*"] hosts = ["houdini"] targets = ["deadline"] label = "Remote Publish ROP enabled" actions = [RepairContextAction] def process(self, context): node = hou.node("/out/REMOTE_PUBLISH") if not node: raise PublishValidationError( "Missing REMOTE_PUBLISH node.", title=self.label) if node.isBypassed(): raise PublishValidationError( "REMOTE_PUBLISH must not be bypassed.", title=self.label) @classmethod def repair(cls, context): """(Re)create the node if it fails to pass validation.""" node = hou.node("/out/REMOTE_PUBLISH") if not node: raise PublishValidationError( "Missing REMOTE_PUBLISH node.", title=cls.label) cls.log.info("Disabling bypass on /out/REMOTE_PUBLISH") node.bypass(False) ================================================ FILE: openpype/hosts/houdini/plugins/publish/validate_review_colorspace.py ================================================ # -*- coding: utf-8 -*- import pyblish.api from openpype.pipeline import ( PublishValidationError, OptionalPyblishPluginMixin ) from openpype.pipeline.publish import RepairAction from openpype.hosts.houdini.api.action import SelectROPAction import os import hou class SetDefaultViewSpaceAction(RepairAction): label = "Set default view colorspace" icon = "mdi.monitor" class ValidateReviewColorspace(pyblish.api.InstancePlugin, OptionalPyblishPluginMixin): """Validate Review Colorspace parameters. It checks if 'OCIO Colorspace' parameter was set to valid value. """ order = pyblish.api.ValidatorOrder + 0.1 families = ["review"] hosts = ["houdini"] label = "Validate Review Colorspace" actions = [SetDefaultViewSpaceAction, SelectROPAction] optional = True def process(self, instance): if not self.is_active(instance.data): return if os.getenv("OCIO") is None: self.log.debug( "Using Houdini's Default Color Management, " " skipping check.." ) return rop_node = hou.node(instance.data["instance_node"]) if rop_node.evalParm("colorcorrect") != 2: # any colorspace settings other than default requires # 'Color Correct' parm to be set to 'OpenColorIO' raise PublishValidationError( "'Color Correction' parm on '{}' ROP must be set to" " 'OpenColorIO'".format(rop_node.path()) ) if rop_node.evalParm("ociocolorspace") not in \ hou.Color.ocio_spaces(): raise PublishValidationError( "Invalid value: Colorspace name doesn't exist.\n" "Check 'OCIO Colorspace' parameter on '{}' ROP" .format(rop_node.path()) ) @classmethod def repair(cls, instance): """Set Default View Space Action. It is a helper action more than a repair action, used to set colorspace on opengl node to the default view. """ from openpype.hosts.houdini.api.colorspace import get_default_display_view_colorspace # noqa rop_node = hou.node(instance.data["instance_node"]) if rop_node.evalParm("colorcorrect") != 2: rop_node.setParms({"colorcorrect": 2}) cls.log.debug( "'Color Correction' parm on '{}' has been set to" " 'OpenColorIO'".format(rop_node.path()) ) # Get default view colorspace name default_view_space = get_default_display_view_colorspace() rop_node.setParms({"ociocolorspace": default_view_space}) cls.log.info( "'OCIO Colorspace' parm on '{}' has been set to " "the default view color space '{}'" .format(rop_node, default_view_space) ) ================================================ FILE: openpype/hosts/houdini/plugins/publish/validate_scene_review.py ================================================ # -*- coding: utf-8 -*- import pyblish.api from openpype.pipeline import PublishValidationError import hou class ValidateSceneReview(pyblish.api.InstancePlugin): """Validator Some Scene Settings before publishing the review 1. Scene Path 2. Resolution """ order = pyblish.api.ValidatorOrder families = ["review"] hosts = ["houdini"] label = "Scene Setting for review" def process(self, instance): report = [] instance_node = hou.node(instance.data.get("instance_node")) invalid = self.get_invalid_scene_path(instance_node) if invalid: report.append(invalid) invalid = self.get_invalid_camera_path(instance_node) if invalid: report.append(invalid) invalid = self.get_invalid_resolution(instance_node) if invalid: report.extend(invalid) if report: raise PublishValidationError( "\n\n".join(report), title=self.label) def get_invalid_scene_path(self, rop_node): scene_path_parm = rop_node.parm("scenepath") scene_path_node = scene_path_parm.evalAsNode() if not scene_path_node: path = scene_path_parm.evalAsString() return "Scene path does not exist: '{}'".format(path) def get_invalid_camera_path(self, rop_node): camera_path_parm = rop_node.parm("camera") camera_node = camera_path_parm.evalAsNode() path = camera_path_parm.evalAsString() if not camera_node: return "Camera path does not exist: '{}'".format(path) type_name = camera_node.type().name() if type_name != "cam": return "Camera path is not a camera: '{}' (type: {})".format( path, type_name ) def get_invalid_resolution(self, rop_node): # The resolution setting is only used when Override Camera Resolution # is enabled. So we skip validation if it is disabled. override = rop_node.parm("tres").eval() if not override: return invalid = [] res_width = rop_node.parm("res1").eval() res_height = rop_node.parm("res2").eval() if res_width == 0: invalid.append("Override Resolution width is set to zero.") if res_height == 0: invalid.append("Override Resolution height is set to zero") return invalid ================================================ FILE: openpype/hosts/houdini/plugins/publish/validate_sop_output_node.py ================================================ # -*- coding: utf-8 -*- import pyblish.api from openpype.pipeline import PublishValidationError from openpype.hosts.houdini.api.action import ( SelectInvalidAction, SelectROPAction, ) import hou class ValidateSopOutputNode(pyblish.api.InstancePlugin): """Validate the instance SOP Output Node. This will ensure: - The SOP Path is set. - The SOP Path refers to an existing object. - The SOP Path node is a SOP node. - The SOP Path node has at least one input connection (has an input) - The SOP Path has geometry data. """ order = pyblish.api.ValidatorOrder families = ["pointcache", "vdbcache"] hosts = ["houdini"] label = "Validate Output Node (SOP)" actions = [SelectROPAction, SelectInvalidAction] def process(self, instance): invalid = self.get_invalid(instance) if invalid: raise PublishValidationError( "Output node(s) are incorrect", title="Invalid output node(s)" ) @classmethod def get_invalid(cls, instance): output_node = instance.data.get("output_node") if output_node is None: node = hou.node(instance.data["instance_node"]) cls.log.error( "SOP Output node in '%s' does not exist. " "Ensure a valid SOP output path is set." % node.path() ) return [node] # Output node must be a Sop node. if not isinstance(output_node, hou.SopNode): cls.log.error( "Output node %s is not a SOP node. " "SOP Path must point to a SOP node, " "instead found category type: %s" % (output_node.path(), output_node.type().category().name()) ) return [output_node] # For the sake of completeness also assert the category type # is Sop to avoid potential edge case scenarios even though # the isinstance check above should be stricter than this category if output_node.type().category().name() != "Sop": raise PublishValidationError( ("Output node {} is not of category Sop. " "This is a bug.").format(output_node.path()), title=cls.label) # Ensure the node is cooked and succeeds to cook so we can correctly # check for its geometry data. if output_node.needsToCook(): cls.log.debug("Cooking node: %s" % output_node.path()) try: output_node.cook() except hou.Error as exc: cls.log.error("Cook failed: %s" % exc) cls.log.error(output_node.errors()[0]) return [output_node] # Ensure the output node has at least Geometry data if not output_node.geometry(): cls.log.error( "Output node `%s` has no geometry data." % output_node.path() ) return [output_node] ================================================ FILE: openpype/hosts/houdini/plugins/publish/validate_subset_name.py ================================================ # -*- coding: utf-8 -*- """Validator for correct naming of Static Meshes.""" import pyblish.api from openpype.pipeline import ( PublishValidationError, OptionalPyblishPluginMixin ) from openpype.pipeline.publish import ( ValidateContentsOrder, RepairAction, ) from openpype.hosts.houdini.api.action import SelectInvalidAction from openpype.pipeline.create import get_subset_name import hou class FixSubsetNameAction(RepairAction): label = "Fix Subset Name" class ValidateSubsetName(pyblish.api.InstancePlugin, OptionalPyblishPluginMixin): """Validate Subset name. """ families = ["staticMesh"] hosts = ["houdini"] label = "Validate Subset Name" order = ValidateContentsOrder + 0.1 actions = [FixSubsetNameAction, SelectInvalidAction] optional = True def process(self, instance): if not self.is_active(instance.data): return invalid = self.get_invalid(instance) if invalid: nodes = [n.path() for n in invalid] raise PublishValidationError( "See log for details. " "Invalid nodes: {0}".format(nodes) ) @classmethod def get_invalid(cls, instance): invalid = [] rop_node = hou.node(instance.data["instance_node"]) # Check subset name asset_doc = instance.data["assetEntity"] subset_name = get_subset_name( family=instance.data["family"], variant=instance.data["variant"], task_name=instance.data["task"], asset_doc=asset_doc, dynamic_data={"asset": asset_doc["name"]} ) if instance.data.get("subset") != subset_name: invalid.append(rop_node) cls.log.error( "Invalid subset name on rop node '%s' should be '%s'.", rop_node.path(), subset_name ) return invalid @classmethod def repair(cls, instance): rop_node = hou.node(instance.data["instance_node"]) # Check subset name asset_doc = instance.data["assetEntity"] subset_name = get_subset_name( family=instance.data["family"], variant=instance.data["variant"], task_name=instance.data["task"], asset_doc=asset_doc, dynamic_data={"asset": asset_doc["name"]} ) instance.data["subset"] = subset_name rop_node.parm("subset").set(subset_name) cls.log.debug( "Subset name on rop node '%s' has been set to '%s'.", rop_node.path(), subset_name ) ================================================ FILE: openpype/hosts/houdini/plugins/publish/validate_unreal_staticmesh_naming.py ================================================ # -*- coding: utf-8 -*- """Validator for correct naming of Static Meshes.""" import pyblish.api from openpype.pipeline import ( PublishValidationError, OptionalPyblishPluginMixin ) from openpype.pipeline.publish import ValidateContentsOrder from openpype.hosts.houdini.api.action import SelectInvalidAction from openpype.hosts.houdini.api.lib import get_output_children import hou class ValidateUnrealStaticMeshName(pyblish.api.InstancePlugin, OptionalPyblishPluginMixin): """Validate name of Unreal Static Mesh. This validator checks if output node name has a collision prefix: - UBX - UCP - USP - UCX This validator also checks if subset name is correct - {static mesh prefix}_{Asset-Name}{Variant}. """ families = ["staticMesh"] hosts = ["houdini"] label = "Unreal Static Mesh Name (FBX)" order = ValidateContentsOrder + 0.1 actions = [SelectInvalidAction] optional = True collision_prefixes = [] static_mesh_prefix = "" @classmethod def apply_settings(cls, project_settings, system_settings): settings = ( project_settings["houdini"]["create"]["CreateStaticMesh"] ) cls.collision_prefixes = settings["collision_prefixes"] cls.static_mesh_prefix = settings["static_mesh_prefix"] def process(self, instance): if not self.is_active(instance.data): return invalid = self.get_invalid(instance) if invalid: nodes = [n.path() for n in invalid] raise PublishValidationError( "See log for details. " "Invalid nodes: {0}".format(nodes) ) @classmethod def get_invalid(cls, instance): invalid = [] rop_node = hou.node(instance.data["instance_node"]) output_node = instance.data.get("output_node") if output_node is None: cls.log.debug( "No Output Node, skipping check.." ) return if rop_node.evalParm("buildfrompath"): # This validator doesn't support naming check if # building hierarchy from path' is used cls.log.info( "Using 'Build Hierarchy from Path Attribute', skipping check.." ) return # Check nodes names all_outputs = get_output_children(output_node, include_sops=False) for output in all_outputs: for prefix in cls.collision_prefixes: if output.name().startswith(prefix): invalid.append(output) cls.log.error( "Invalid node name: Node '%s' " "includes a collision prefix '%s'", output.path(), prefix ) break return invalid ================================================ FILE: openpype/hosts/houdini/plugins/publish/validate_usd_layer_path_backslashes.py ================================================ # -*- coding: utf-8 -*- import pyblish.api import openpype.hosts.houdini.api.usd as hou_usdlib from openpype.pipeline import PublishValidationError import hou class ValidateUSDLayerPathBackslashes(pyblish.api.InstancePlugin): """Validate USD loaded paths have no backslashes. This is a crucial validation for HUSK USD rendering as Houdini's USD Render ROP will fail to write out a .usd file for rendering that correctly preserves the backslashes, e.g. it will incorrectly convert a '\t' to a TAB character disallowing HUSK to find those specific files. This validation is redundant for usdModel since that flattens the model before write. As such it will never have any used layers with a path. """ order = pyblish.api.ValidatorOrder families = ["usdSetDress", "usdShade", "usd", "usdrender"] hosts = ["houdini"] label = "USD Layer path backslashes" optional = True def process(self, instance): rop = hou.node(instance.data.get("instance_node")) lop_path = hou_usdlib.get_usd_rop_loppath(rop) stage = lop_path.stage(apply_viewport_overrides=False) invalid = [] for layer in stage.GetUsedLayers(): references = layer.externalReferences for ref in references: # Ignore anonymous layers if ref.startswith("anon:"): continue # If any backslashes in the path consider it invalid if "\\" in ref: self.log.error("Found invalid path: %s" % ref) invalid.append(layer) if invalid: raise PublishValidationError(( "Loaded layers have backslashes. " "This is invalid for HUSK USD rendering."), title=self.label) ================================================ FILE: openpype/hosts/houdini/plugins/publish/validate_usd_model_and_shade.py ================================================ # -*- coding: utf-8 -*- import pyblish.api import openpype.hosts.houdini.api.usd as hou_usdlib from openpype.pipeline import PublishValidationError from pxr import UsdShade, UsdRender, UsdLux import hou def fullname(o): """Get fully qualified class name""" module = o.__module__ if module is None or module == str.__module__: return o.__name__ return module + "." + o.__name__ class ValidateUsdModel(pyblish.api.InstancePlugin): """Validate USD Model. Disallow Shaders, Render settings, products and vars and Lux lights. """ order = pyblish.api.ValidatorOrder families = ["usdModel"] hosts = ["houdini"] label = "Validate USD Model" optional = True disallowed = [ UsdShade.Shader, UsdRender.Settings, UsdRender.Product, UsdRender.Var, UsdLux.Light, ] def process(self, instance): rop = hou.node(instance.data.get("instance_node")) lop_path = hou_usdlib.get_usd_rop_loppath(rop) stage = lop_path.stage(apply_viewport_overrides=False) invalid = [] for prim in stage.Traverse(): for klass in self.disallowed: if klass(prim): # Get full class name without pxr. prefix name = fullname(klass).split("pxr.", 1)[-1] path = str(prim.GetPath()) self.log.warning("Disallowed %s: %s" % (name, path)) invalid.append(prim) if invalid: prim_paths = sorted([str(prim.GetPath()) for prim in invalid]) raise PublishValidationError( "Found invalid primitives: {}".format(prim_paths)) class ValidateUsdShade(ValidateUsdModel): """Validate usdShade. Disallow Render settings, products, vars and Lux lights. """ families = ["usdShade"] label = "Validate USD Shade" disallowed = [ UsdRender.Settings, UsdRender.Product, UsdRender.Var, UsdLux.Light, ] ================================================ FILE: openpype/hosts/houdini/plugins/publish/validate_usd_output_node.py ================================================ # -*- coding: utf-8 -*- import pyblish.api from openpype.pipeline import PublishValidationError class ValidateUSDOutputNode(pyblish.api.InstancePlugin): """Validate the instance USD LOPs Output Node. This will ensure: - The LOP Path is set. - The LOP Path refers to an existing object. - The LOP Path node is a LOP node. """ order = pyblish.api.ValidatorOrder families = ["usd"] hosts = ["houdini"] label = "Validate Output Node (USD)" def process(self, instance): invalid = self.get_invalid(instance) if invalid: raise PublishValidationError( ("Output node(s) `{}` are incorrect. " "See plug-in log for details.").format(invalid), title=self.label ) @classmethod def get_invalid(cls, instance): import hou output_node = instance.data["output_node"] if output_node is None: node = hou.node(instance.data.get("instance_node")) cls.log.error( "USD node '%s' LOP path does not exist. " "Ensure a valid LOP path is set." % node.path() ) return [node.path()] # Output node must be a Sop node. if not isinstance(output_node, hou.LopNode): cls.log.error( "Output node %s is not a LOP node. " "LOP Path must point to a LOP node, " "instead found category type: %s" % (output_node.path(), output_node.type().category().name()) ) return [output_node.path()] ================================================ FILE: openpype/hosts/houdini/plugins/publish/validate_usd_render_product_names.py ================================================ # -*- coding: utf-8 -*- import os import pyblish.api from openpype.pipeline import PublishValidationError class ValidateUSDRenderProductNames(pyblish.api.InstancePlugin): """Validate USD Render Product names are correctly set absolute paths.""" order = pyblish.api.ValidatorOrder families = ["usdrender"] hosts = ["houdini"] label = "Validate USD Render Product Names" optional = True def process(self, instance): invalid = [] for filepath in instance.data["files"]: if not filepath: invalid.append("Detected empty output filepath.") if not os.path.isabs(filepath): invalid.append( "Output file path is not absolute path: %s" % filepath ) if invalid: for message in invalid: self.log.error(message) raise PublishValidationError( "USD Render Paths are invalid.", title=self.label) ================================================ FILE: openpype/hosts/houdini/plugins/publish/validate_usd_setdress.py ================================================ # -*- coding: utf-8 -*- import pyblish.api import openpype.hosts.houdini.api.usd as hou_usdlib from openpype.pipeline import PublishValidationError class ValidateUsdSetDress(pyblish.api.InstancePlugin): """Validate USD Set Dress. Must only have references or payloads. May not generate new mesh or flattened meshes. """ order = pyblish.api.ValidatorOrder families = ["usdSetDress"] hosts = ["houdini"] label = "Validate USD Set Dress" optional = True def process(self, instance): from pxr import UsdGeom import hou rop = hou.node(instance.data.get("instance_node")) lop_path = hou_usdlib.get_usd_rop_loppath(rop) stage = lop_path.stage(apply_viewport_overrides=False) invalid = [] for node in stage.Traverse(): if UsdGeom.Mesh(node): # This solely checks whether there is any USD involved # in this Prim's Stack and doesn't accurately tell us # whether it was generated locally or not. # TODO: More accurately track whether the Prim was created # in the local scene stack = node.GetPrimStack() for sdf in stack: path = sdf.layer.realPath if path: break else: prim_path = node.GetPath() self.log.error( "%s is not referenced geometry." % prim_path ) invalid.append(node) if invalid: raise PublishValidationError(( "SetDress contains local geometry. " "This is not allowed, it must be an assembly " "of referenced assets."), title=self.label ) ================================================ FILE: openpype/hosts/houdini/plugins/publish/validate_usd_shade_model_exists.py ================================================ # -*- coding: utf-8 -*- import re import pyblish.api from openpype.client import get_subset_by_name from openpype.pipeline.publish import ValidateContentsOrder from openpype.pipeline import PublishValidationError class ValidateUSDShadeModelExists(pyblish.api.InstancePlugin): """Validate the Instance has no current cooking errors.""" order = ValidateContentsOrder hosts = ["houdini"] families = ["usdShade"] label = "USD Shade model exists" def process(self, instance): project_name = instance.context.data["projectName"] asset_name = instance.data["asset"] subset = instance.data["subset"] # Assume shading variation starts after a dot separator shade_subset = subset.split(".", 1)[0] model_subset = re.sub("^usdShade", "usdModel", shade_subset) asset_doc = instance.data.get("assetEntity") if not asset_doc: raise RuntimeError("Asset document is not filled on instance.") subset_doc = get_subset_by_name( project_name, model_subset, asset_doc["_id"], fields=["_id"] ) if not subset_doc: raise PublishValidationError( ("USD Model subset not found: " "{} ({})").format(model_subset, asset_name), title=self.label ) ================================================ FILE: openpype/hosts/houdini/plugins/publish/validate_usd_shade_workspace.py ================================================ # -*- coding: utf-8 -*- import pyblish.api from openpype.pipeline import PublishValidationError import hou class ValidateUsdShadeWorkspace(pyblish.api.InstancePlugin): """Validate USD Shading Workspace is correct version. There have been some issues with outdated/erroneous Shading Workspaces so this is to confirm everything is set as it should. """ order = pyblish.api.ValidatorOrder hosts = ["houdini"] families = ["usdShade"] label = "USD Shade Workspace" def process(self, instance): rop = hou.node(instance.data.get("instance_node")) workspace = rop.parent() definition = workspace.type().definition() name = definition.nodeType().name() library = definition.libraryFilePath() all_definitions = hou.hda.definitionsInFile(library) node_type, version = name.rsplit(":", 1) version = float(version) highest = version for other_definition in all_definitions: other_name = other_definition.nodeType().name() other_node_type, other_version = other_name.rsplit(":", 1) other_version = float(other_version) if node_type != other_node_type: continue # Get the highest version highest = max(highest, other_version) if version != highest: raise PublishValidationError( ("Shading Workspace is not the latest version." " Found {}. Latest is {}.").format(version, highest), title=self.label ) # There were some issues with the editable node not having the right # configured path. So for now let's assure that is correct to.from value = ( 'avalon://`chs("../asset_name")`/' 'usdShade`chs("../model_variantname1")`.usd' ) rop_value = rop.parm("lopoutput").rawValue() if rop_value != value: raise PublishValidationError( ("Shading Workspace has invalid 'lopoutput'" " parameter value. The Shading Workspace" " needs to be reset to its default values."), title=self.label ) ================================================ FILE: openpype/hosts/houdini/plugins/publish/validate_vdb_output_node.py ================================================ # -*- coding: utf-8 -*- import contextlib import pyblish.api import hou from openpype.pipeline import PublishXmlValidationError from openpype.hosts.houdini.api.action import SelectInvalidAction def group_consecutive_numbers(nums): """ Args: nums (list): List of sorted integer numbers. Yields: str: Group ranges as {start}-{end} if more than one number in the range else it yields {end} """ start = None end = None def _result(a, b): if a == b: return "{}".format(a) else: return "{}-{}".format(a, b) for num in nums: if start is None: start = num end = num elif num == end + 1: end = num else: yield _result(start, end) start = num end = num if start is not None: yield _result(start, end) @contextlib.contextmanager def update_mode_context(mode): original = hou.updateModeSetting() try: hou.setUpdateMode(mode) yield finally: hou.setUpdateMode(original) def get_geometry_at_frame(sop_node, frame, force=True): """Return geometry at frame but force a cooked value.""" if not hasattr(sop_node, "geometry"): return with update_mode_context(hou.updateMode.AutoUpdate): sop_node.cook(force=force, frame_range=(frame, frame)) return sop_node.geometryAtFrame(frame) class ValidateVDBOutputNode(pyblish.api.InstancePlugin): """Validate that the node connected to the output node is of type VDB. All primitives of the output geometry must be VDBs, no other primitive types are allowed. That means that regardless of the amount of VDBs in the geometry it will have an equal amount of VDBs, points, primitives and vertices since each VDB primitive is one point, one vertex and one VDB. This validation only checks the geometry on the first frame of the export frame range for optimization purposes. A VDB is an inherited type of Prim, holds the following data: - Primitives: 1 - Points: 1 - Vertices: 1 - VDBs: 1 """ order = pyblish.api.ValidatorOrder + 0.1 families = ["vdbcache"] hosts = ["houdini"] label = "Validate Output Node (VDB)" actions = [SelectInvalidAction] def process(self, instance): invalid_nodes, message = self.get_invalid_with_message(instance) if invalid_nodes: # instance_node is str, but output_node is hou.Node so we convert output = instance.data.get("output_node") output_path = output.path() if output else None raise PublishXmlValidationError( self, "Invalid VDB content: {}".format(message), formatting_data={ "message": message, "rop_path": instance.data.get("instance_node"), "sop_path": output_path } ) @classmethod def get_invalid_with_message(cls, instance): node = instance.data.get("output_node") if node is None: instance_node = instance.data.get("instance_node") error = ( "SOP path is not correctly set on " "ROP node `{}`.".format(instance_node) ) return [hou.node(instance_node), error] frame = instance.data.get("frameStart", 0) geometry = get_geometry_at_frame(node, frame) if geometry is None: # No geometry data on this node, maybe the node hasn't cooked? error = ( "SOP node `{}` has no geometry data. " "Was it unable to cook?".format(node.path()) ) return [node, error] num_prims = geometry.intrinsicValue("primitivecount") num_points = geometry.intrinsicValue("pointcount") if num_prims == 0 and num_points == 0: # Since we are only checking the first frame it doesn't mean there # won't be VDB prims in a few frames. As such we'll assume for now # the user knows what he or she is doing cls.log.warning( "SOP node `{}` has no primitives on start frame {}. " "Validation is skipped and it is assumed elsewhere in the " "frame range VDB prims and only VDB prims will exist." "".format(node.path(), int(frame)) ) return [None, None] num_vdb_prims = geometry.countPrimType(hou.primType.VDB) cls.log.debug("Detected {} VDB primitives".format(num_vdb_prims)) if num_prims != num_vdb_prims: # There's at least one primitive that is not a VDB. # Search them and report them to the artist. prims = geometry.prims() invalid_prims = [prim for prim in prims if not isinstance(prim, hou.VDB)] if invalid_prims: # Log prim numbers as consecutive ranges so logging isn't very # slow for large number of primitives error = ( "Found non-VDB primitives for `{}`. " "Primitive indices {} are not VDB primitives.".format( node.path(), ", ".join(group_consecutive_numbers( prim.number() for prim in invalid_prims )) ) ) return [node, error] if num_points != num_vdb_prims: # We have points unrelated to the VDB primitives. error = ( "The number of primitives and points do not match in '{}'. " "This likely means you have unconnected points, which we do " "not allow in the VDB output.".format(node.path())) return [node, error] return [None, None] @classmethod def get_invalid(cls, instance): nodes, _ = cls.get_invalid_with_message(instance) return nodes ================================================ FILE: openpype/hosts/houdini/plugins/publish/validate_workfile_paths.py ================================================ # -*- coding: utf-8 -*- import pyblish.api import hou from openpype.pipeline import ( PublishValidationError, OptionalPyblishPluginMixin ) from openpype.pipeline.publish import RepairAction class ValidateWorkfilePaths( pyblish.api.InstancePlugin, OptionalPyblishPluginMixin): """Validate workfile paths so they are absolute.""" order = pyblish.api.ValidatorOrder families = ["workfile"] hosts = ["houdini"] label = "Validate Workfile Paths" actions = [RepairAction] optional = True node_types = ["file", "alembic"] prohibited_vars = ["$HIP", "$JOB"] def process(self, instance): if not self.is_active(instance.data): return invalid = self.get_invalid() self.log.debug( "Checking node types: {}".format(", ".join(self.node_types))) self.log.debug( "Searching prohibited vars: {}".format( ", ".join(self.prohibited_vars) ) ) if invalid: all_container_vars = set() for param in invalid: value = param.unexpandedString() contained_vars = [ var for var in self.prohibited_vars if var in value ] all_container_vars.update(contained_vars) self.log.error( "Parm {} contains prohibited vars {}: {}".format( param.path(), ", ".join(contained_vars), value) ) message = ( "Prohibited vars {} found in parameter values".format( ", ".join(all_container_vars) ) ) raise PublishValidationError(message, title=self.label) @classmethod def get_invalid(cls): invalid = [] for param, _ in hou.fileReferences(): # it might return None for some reason if not param: continue # skip nodes we are not interested in if param.node().type().name() not in cls.node_types: continue if any( v for v in cls.prohibited_vars if v in param.unexpandedString()): invalid.append(param) return invalid @classmethod def repair(cls, instance): invalid = cls.get_invalid() for param in invalid: cls.log.info("Processing: {}".format(param.path())) cls.log.info("Replacing {} for {}".format( param.unexpandedString(), hou.text.expandString(param.unexpandedString()))) param.set(hou.text.expandString(param.unexpandedString())) ================================================ FILE: openpype/hosts/houdini/startup/MainMenuCommon.xml ================================================ Invalid VDB ## Invalid VDB output All primitives of the output geometry must be VDBs, no other primitive types are allowed. That means that regardless of the amount of VDBs in the geometry it will have an equal amount of VDBs, points, primitives and vertices since each VDB primitive is one point, one vertex and one VDB. This validation only checks the geometry on the first frame of the export frame range. ### Detailed Info ROP node `{rop_path}` is set to export SOP path `{sop_path}`. {message} ================================================ FILE: openpype/hosts/houdini/startup/python2.7libs/pythonrc.py ================================================ # -*- coding: utf-8 -*- """OpenPype startup script.""" from openpype.pipeline import install_host from openpype.hosts.houdini.api import HoudiniHost from openpype import AYON_SERVER_ENABLED def main(): print("Installing {} ...".format( "AYON" if AYON_SERVER_ENABLED else "OpenPype")) install_host(HoudiniHost()) main() ================================================ FILE: openpype/hosts/houdini/startup/python3.10libs/pythonrc.py ================================================ # -*- coding: utf-8 -*- """OpenPype startup script.""" from openpype.pipeline import install_host from openpype.hosts.houdini.api import HoudiniHost from openpype import AYON_SERVER_ENABLED def main(): print("Installing {} ...".format( "AYON" if AYON_SERVER_ENABLED else "OpenPype")) install_host(HoudiniHost()) main() ================================================ FILE: openpype/hosts/houdini/startup/python3.7libs/pythonrc.py ================================================ # -*- coding: utf-8 -*- """OpenPype startup script.""" from openpype.pipeline import install_host from openpype.hosts.houdini.api import HoudiniHost from openpype import AYON_SERVER_ENABLED def main(): print("Installing {} ...".format( "AYON" if AYON_SERVER_ENABLED else "OpenPype")) install_host(HoudiniHost()) main() ================================================ FILE: openpype/hosts/houdini/startup/python3.9libs/pythonrc.py ================================================ # -*- coding: utf-8 -*- """OpenPype startup script.""" from openpype.pipeline import install_host from openpype.hosts.houdini.api import HoudiniHost from openpype import AYON_SERVER_ENABLED def main(): print("Installing {} ...".format( "AYON" if AYON_SERVER_ENABLED else "OpenPype")) install_host(HoudiniHost()) main() ================================================ FILE: openpype/hosts/max/__init__.py ================================================ from .addon import ( MaxAddon, MAX_HOST_DIR, ) __all__ = ( "MaxAddon", "MAX_HOST_DIR", ) ================================================ FILE: openpype/hosts/max/addon.py ================================================ # -*- coding: utf-8 -*- import os from openpype.modules import OpenPypeModule, IHostAddon MAX_HOST_DIR = os.path.dirname(os.path.abspath(__file__)) class MaxAddon(OpenPypeModule, IHostAddon): name = "max" host_name = "max" def initialize(self, module_settings): self.enabled = True def add_implementation_envs(self, env, _app): # Remove auto screen scale factor for Qt # - let 3dsmax decide it's value env.pop("QT_AUTO_SCREEN_SCALE_FACTOR", None) def get_workfile_extensions(self): return [".max"] def get_launch_hook_paths(self, app): if app.host_name != self.host_name: return [] return [ os.path.join(MAX_HOST_DIR, "hooks") ] ================================================ FILE: openpype/hosts/max/api/__init__.py ================================================ # -*- coding: utf-8 -*- """Public API for 3dsmax""" from .pipeline import ( MaxHost, ) from .lib import ( maintained_selection, lsattr, get_all_children ) __all__ = [ "MaxHost", "maintained_selection", "lsattr", "get_all_children" ] ================================================ FILE: openpype/hosts/max/api/action.py ================================================ from pymxs import runtime as rt import pyblish.api from openpype.pipeline.publish import get_errored_instances_from_context class SelectInvalidAction(pyblish.api.Action): """Select invalid objects in Blender when a publish plug-in failed.""" label = "Select Invalid" on = "failed" icon = "search" def process(self, context, plugin): errored_instances = get_errored_instances_from_context(context, plugin=plugin) # Get the invalid nodes for the plug-ins self.log.info("Finding invalid nodes...") invalid = list() for instance in errored_instances: invalid_nodes = plugin.get_invalid(instance) if invalid_nodes: if isinstance(invalid_nodes, (list, tuple)): invalid.extend(invalid_nodes) else: self.log.warning( "Failed plug-in doesn't have any selectable objects." ) if not invalid: self.log.info("No invalid nodes found.") return invalid_names = [obj.name for obj in invalid if isinstance(obj, str)] if not invalid_names: invalid_names = [obj.name for obj, _ in invalid] invalid = [obj for obj, _ in invalid] self.log.info( "Selecting invalid objects: %s", ", ".join(invalid_names) ) rt.Select(invalid) ================================================ FILE: openpype/hosts/max/api/colorspace.py ================================================ import attr from pymxs import runtime as rt @attr.s class LayerMetadata(object): """Data class for Render Layer metadata.""" frameStart = attr.ib() frameEnd = attr.ib() @attr.s class RenderProduct(object): """Getting Colorspace as Specific Render Product Parameter for submitting publish job. """ colorspace = attr.ib() # colorspace view = attr.ib() productName = attr.ib(default=None) class ARenderProduct(object): def __init__(self): """Constructor.""" # Initialize self.layer_data = self._get_layer_data() self.layer_data.products = self.get_colorspace_data() def _get_layer_data(self): return LayerMetadata( frameStart=int(rt.rendStart), frameEnd=int(rt.rendEnd), ) def get_colorspace_data(self): """To be implemented by renderer class. This should return a list of RenderProducts. Returns: list: List of RenderProduct """ colorspace_data = [ RenderProduct( colorspace="sRGB", view="ACES 1.0", productName="" ) ] return colorspace_data ================================================ FILE: openpype/hosts/max/api/lib.py ================================================ # -*- coding: utf-8 -*- """Library of functions useful for 3dsmax pipeline.""" import contextlib import logging import json from typing import Any, Dict, Union import six from openpype.pipeline import get_current_project_name, colorspace from openpype.settings import get_project_settings from openpype.pipeline.context_tools import ( get_current_project, get_current_project_asset) from openpype.style import load_stylesheet from pymxs import runtime as rt JSON_PREFIX = "JSON::" log = logging.getLogger("openpype.hosts.max") def get_main_window(): """Acquire Max's main window""" from qtpy import QtWidgets top_widgets = QtWidgets.QApplication.topLevelWidgets() name = "QmaxApplicationWindow" for widget in top_widgets: if ( widget.inherits("QMainWindow") and widget.metaObject().className() == name ): return widget raise RuntimeError('Count not find 3dsMax main window.') def imprint(node_name: str, data: dict) -> bool: node = rt.GetNodeByName(node_name) if not node: return False for k, v in data.items(): if isinstance(v, (dict, list)): rt.SetUserProp(node, k, f"{JSON_PREFIX}{json.dumps(v)}") else: rt.SetUserProp(node, k, v) return True def lsattr( attr: str, value: Union[str, None] = None, root: Union[str, None] = None) -> list: """List nodes having attribute with specified value. Args: attr (str): Attribute name to match. value (str, Optional): Value to match, of omitted, all nodes with specified attribute are returned no matter of value. root (str, Optional): Root node name. If omitted, scene root is used. Returns: list of nodes. """ root = rt.RootNode if root is None else rt.GetNodeByName(root) def output_node(node, nodes): nodes.append(node) for child in node.Children: output_node(child, nodes) nodes = [] output_node(root, nodes) return [ n for n in nodes if rt.GetUserProp(n, attr) == value ] if value else [ n for n in nodes if rt.GetUserProp(n, attr) ] def read(container) -> dict: data = {} props = rt.GetUserPropBuffer(container) # this shouldn't happen but let's guard against it anyway if not props: return data for line in props.split("\r\n"): try: key, value = line.split("=") except ValueError: # if the line cannot be split we can't really parse it continue value = value.strip() if isinstance(value.strip(), six.string_types) and \ value.startswith(JSON_PREFIX): with contextlib.suppress(json.JSONDecodeError): value = json.loads(value[len(JSON_PREFIX):]) # default value behavior # convert maxscript boolean values if value == "true": value = True elif value == "false": value = False data[key.strip()] = value data["instance_node"] = container.Name return data @contextlib.contextmanager def maintained_selection(): previous_selection = rt.GetCurrentSelection() try: yield finally: if previous_selection: rt.Select(previous_selection) else: rt.Select() def get_all_children(parent, node_type=None): """Handy function to get all the children of a given node Args: parent (3dsmax Node1): Node to get all children of. node_type (None, runtime.class): give class to check for e.g. rt.FFDBox/rt.GeometryClass etc. Returns: list: list of all children of the parent node """ def list_children(node): children = [] for c in node.Children: children.append(c) children = children + list_children(c) return children child_list = list_children(parent) return ([x for x in child_list if rt.SuperClassOf(x) == node_type] if node_type else child_list) def get_current_renderer(): """ Notes: Get current renderer for Max Returns: "{Current Renderer}:{Current Renderer}" e.g. "Redshift_Renderer:Redshift_Renderer" """ return rt.renderers.production def get_default_render_folder(project_setting=None): return (project_setting["max"] ["RenderSettings"] ["default_render_image_folder"]) def set_render_frame_range(start_frame, end_frame): """ Note: Frame range can be specified in different types. Possible values are: * `1` - Single frame. * `2` - Active time segment ( animationRange ). * `3` - User specified Range. * `4` - User specified Frame pickup string (for example `1,3,5-12`). Todo: Current type is hard-coded, there should be a custom setting for this. """ rt.rendTimeType = 3 if start_frame is not None and end_frame is not None: rt.rendStart = int(start_frame) rt.rendEnd = int(end_frame) def get_multipass_setting(project_setting=None): return (project_setting["max"] ["RenderSettings"] ["multipass"]) def set_scene_resolution(width: int, height: int): """Set the render resolution Args: width(int): value of the width height(int): value of the height Returns: None """ # make sure the render dialog is closed # for the update of resolution # Changing the Render Setup dialog settings should be done # with the actual Render Setup dialog in a closed state. if rt.renderSceneDialog.isOpen(): rt.renderSceneDialog.close() rt.renderWidth = width rt.renderHeight = height def reset_scene_resolution(): """Apply the scene resolution from the project definition scene resolution can be overwritten by an asset if the asset.data contains any information regarding scene resolution . Returns: None """ data = ["data.resolutionWidth", "data.resolutionHeight"] project_resolution = get_current_project(fields=data) project_resolution_data = project_resolution["data"] asset_resolution = get_current_project_asset(fields=data) asset_resolution_data = asset_resolution["data"] # Set project resolution project_width = int(project_resolution_data.get("resolutionWidth", 1920)) project_height = int(project_resolution_data.get("resolutionHeight", 1080)) width = int(asset_resolution_data.get("resolutionWidth", project_width)) height = int(asset_resolution_data.get("resolutionHeight", project_height)) set_scene_resolution(width, height) def get_frame_range(asset_doc=None) -> Union[Dict[str, Any], None]: """Get the current assets frame range and handles. Args: asset_doc (dict): Asset Entity Data Returns: dict: with frame start, frame end, handle start, handle end. """ # Set frame start/end if asset_doc is None: asset_doc = get_current_project_asset() data = asset_doc["data"] frame_start = data.get("frameStart") frame_end = data.get("frameEnd") if frame_start is None or frame_end is None: return {} frame_start = int(frame_start) frame_end = int(frame_end) handle_start = int(data.get("handleStart", 0)) handle_end = int(data.get("handleEnd", 0)) frame_start_handle = frame_start - handle_start frame_end_handle = frame_end + handle_end return { "frameStart": frame_start, "frameEnd": frame_end, "handleStart": handle_start, "handleEnd": handle_end, "frameStartHandle": frame_start_handle, "frameEndHandle": frame_end_handle, } def reset_frame_range(fps: bool = True): """Set frame range to current asset. This is part of 3dsmax documentation: animationRange: A System Global variable which lets you get and set an Interval value that defines the start and end frames of the Active Time Segment. frameRate: A System Global variable which lets you get and set an Integer value that defines the current scene frame rate in frames-per-second. """ if fps: data_fps = get_current_project(fields=["data.fps"]) fps_number = float(data_fps["data"]["fps"]) rt.frameRate = fps_number frame_range = get_frame_range() set_timeline( frame_range["frameStartHandle"], frame_range["frameEndHandle"]) set_render_frame_range( frame_range["frameStartHandle"], frame_range["frameEndHandle"]) def reset_unit_scale(): """Apply the unit scale setting to 3dsMax """ project_name = get_current_project_name() settings = get_project_settings(project_name).get("max") scene_scale = settings.get("unit_scale_settings", {}).get("scene_unit_scale") if scene_scale: rt.units.DisplayType = rt.Name("Metric") rt.units.MetricType = rt.Name(scene_scale) else: rt.units.DisplayType = rt.Name("Generic") def convert_unit_scale(): """Convert system unit scale in 3dsMax for fbx export Returns: str: unit scale """ unit_scale_dict = { "millimeters": "mm", "centimeters": "cm", "meters": "m", "kilometers": "km" } current_unit_scale = rt.Execute("units.MetricType as string") return unit_scale_dict[current_unit_scale] def set_context_setting(): """Apply the project settings from the project definition Settings can be overwritten by an asset if the asset.data contains any information regarding those settings. Examples of settings: frame range resolution Returns: None """ reset_scene_resolution() reset_frame_range() reset_colorspace() reset_unit_scale() def get_max_version(): """ Args: get max version date for deadline Returns: #(25000, 62, 0, 25, 0, 0, 997, 2023, "") max_info[7] = max version date """ max_info = rt.MaxVersion() return max_info[7] def is_headless(): """Check if 3dsMax runs in batch mode. If it returns True, it runs in 3dsbatch.exe If it returns False, it runs in 3dsmax.exe """ return rt.maxops.isInNonInteractiveMode() def set_timeline(frameStart, frameEnd): """Set frame range for timeline editor in Max """ rt.animationRange = rt.interval(frameStart, frameEnd) return rt.animationRange def reset_colorspace(): """OCIO Configuration Supports in 3dsMax 2024+ """ if int(get_max_version()) < 2024: return project_name = get_current_project_name() colorspace_mgr = rt.ColorPipelineMgr project_settings = get_project_settings(project_name) max_config_data = colorspace.get_imageio_config( project_name, "max", project_settings) if max_config_data: ocio_config_path = max_config_data["path"] colorspace_mgr = rt.ColorPipelineMgr colorspace_mgr.Mode = rt.Name("OCIO_Custom") colorspace_mgr.OCIOConfigPath = ocio_config_path def check_colorspace(): parent = get_main_window() if parent is None: log.info("Skipping outdated pop-up " "because Max main window can't be found.") if int(get_max_version()) >= 2024: color_mgr = rt.ColorPipelineMgr project_name = get_current_project_name() project_settings = get_project_settings(project_name) max_config_data = colorspace.get_imageio_config( project_name, "max", project_settings) if max_config_data and color_mgr.Mode != rt.Name("OCIO_Custom"): if not is_headless(): from openpype.widgets import popup dialog = popup.Popup(parent=parent) dialog.setWindowTitle("Warning: Wrong OCIO Mode") dialog.setMessage("This scene has wrong OCIO " "Mode setting.") dialog.setButtonText("Fix") dialog.setStyleSheet(load_stylesheet()) dialog.on_clicked.connect(reset_colorspace) dialog.show() def unique_namespace(namespace, format="%02d", prefix="", suffix="", con_suffix="CON"): """Return unique namespace Arguments: namespace (str): Name of namespace to consider format (str, optional): Formatting of the given iteration number suffix (str, optional): Only consider namespaces with this suffix. con_suffix: max only, for finding the name of the master container >>> unique_namespace("bar") # bar01 >>> unique_namespace(":hello") # :hello01 >>> unique_namespace("bar:", suffix="_NS") # bar01_NS: """ def current_namespace(): current = namespace # When inside a namespace Max adds no trailing : if not current.endswith(":"): current += ":" return current # Always check against the absolute namespace root # There's no clash with :x if we're defining namespace :a:x ROOT = ":" if namespace.startswith(":") else current_namespace() # Strip trailing `:` tokens since we might want to add a suffix start = ":" if namespace.startswith(":") else "" end = ":" if namespace.endswith(":") else "" namespace = namespace.strip(":") if ":" in namespace: # Split off any nesting that we don't uniqify anyway. parents, namespace = namespace.rsplit(":", 1) start += parents + ":" ROOT += start iteration = 1 increment_version = True while increment_version: nr_namespace = namespace + format % iteration unique = prefix + nr_namespace + suffix container_name = f"{unique}:{namespace}{con_suffix}" if not rt.getNodeByName(container_name): name_space = start + unique + end increment_version = False return name_space else: increment_version = True iteration += 1 def get_namespace(container_name): """Get the namespace and name of the sub-container Args: container_name (str): the name of master container Raises: RuntimeError: when there is no master container found Returns: namespace (str): namespace of the sub-container name (str): name of the sub-container """ node = rt.getNodeByName(container_name) if not node: raise RuntimeError("Master Container Not Found..") name = rt.getUserProp(node, "name") namespace = rt.getUserProp(node, "namespace") return namespace, name def object_transform_set(container_children): """A function which allows to store the transform of previous loaded object(s) Args: container_children(list): A list of nodes Returns: transform_set (dict): A dict with all transform data of the previous loaded object(s) """ transform_set = {} for node in container_children: name = f"{node.name}.transform" transform_set[name] = node.pos name = f"{node.name}.scale" transform_set[name] = node.scale return transform_set def get_plugins() -> list: """Get all loaded plugins in 3dsMax Returns: plugin_info_list: a list of loaded plugins """ manager = rt.PluginManager count = manager.pluginDllCount plugin_info_list = [] for p in range(1, count + 1): plugin_info = manager.pluginDllName(p) plugin_info_list.append(plugin_info) return plugin_info_list @contextlib.contextmanager def render_resolution(width, height): """Set render resolution option during context Args: width (int): render width height (int): render height """ current_renderWidth = rt.renderWidth current_renderHeight = rt.renderHeight try: rt.renderWidth = width rt.renderHeight = height yield finally: rt.renderWidth = current_renderWidth rt.renderHeight = current_renderHeight @contextlib.contextmanager def suspended_refresh(): """Suspended refresh for scene and modify panel redraw. """ if is_headless(): yield return rt.disableSceneRedraw() rt.suspendEditing() try: yield finally: rt.enableSceneRedraw() rt.resumeEditing() ================================================ FILE: openpype/hosts/max/api/lib_renderproducts.py ================================================ # Render Element Example : For scanline render, VRay # https://help.autodesk.com/view/MAXDEV/2022/ENU/?guid=GUID-E8F75D47-B998-4800-A3A5-610E22913CFC # arnold # https://help.autodesk.com/view/ARNOL/ENU/?guid=arnold_for_3ds_max_ax_maxscript_commands_ax_renderview_commands_html import os from pymxs import runtime as rt from openpype.hosts.max.api.lib import get_current_renderer from openpype.pipeline import get_current_project_name from openpype.settings import get_project_settings class RenderProducts(object): def __init__(self, project_settings=None): self._project_settings = project_settings if not self._project_settings: self._project_settings = get_project_settings( get_current_project_name() ) def get_beauty(self, container): render_dir = os.path.dirname(rt.rendOutputFilename) output_file = os.path.join(render_dir, container) setting = self._project_settings img_fmt = setting["max"]["RenderSettings"]["image_format"] # noqa start_frame = int(rt.rendStart) end_frame = int(rt.rendEnd) + 1 return { "beauty": self.get_expected_beauty( output_file, start_frame, end_frame, img_fmt ) } def get_multiple_beauty(self, outputs, cameras): beauty_output_frames = dict() for output, camera in zip(outputs, cameras): filename, ext = os.path.splitext(output) filename = filename.replace(".", "") ext = ext.replace(".", "") start_frame = int(rt.rendStart) end_frame = int(rt.rendEnd) + 1 new_beauty = self.get_expected_beauty( filename, start_frame, end_frame, ext ) beauty_output = ({ f"{camera}_beauty": new_beauty }) beauty_output_frames.update(beauty_output) return beauty_output_frames def get_multiple_aovs(self, outputs, cameras): renderer_class = get_current_renderer() renderer = str(renderer_class).split(":")[0] aovs_frames = {} for output, camera in zip(outputs, cameras): filename, ext = os.path.splitext(output) filename = filename.replace(".", "") ext = ext.replace(".", "") start_frame = int(rt.rendStart) end_frame = int(rt.rendEnd) + 1 if renderer in [ "ART_Renderer", "V_Ray_6_Hotfix_3", "V_Ray_GPU_6_Hotfix_3", "Default_Scanline_Renderer", "Quicksilver_Hardware_Renderer", ]: render_name = self.get_render_elements_name() if render_name: for name in render_name: aovs_frames.update({ f"{camera}_{name}": self.get_expected_aovs( filename, name, start_frame, end_frame, ext) }) elif renderer == "Redshift_Renderer": render_name = self.get_render_elements_name() if render_name: rs_aov_files = rt.Execute("renderers.current.separateAovFiles") # noqa # this doesn't work, always returns False # rs_AovFiles = rt.RedShift_Renderer().separateAovFiles if ext == "exr" and not rs_aov_files: for name in render_name: if name == "RsCryptomatte": aovs_frames.update({ f"{camera}_{name}": self.get_expected_aovs( filename, name, start_frame, end_frame, ext) }) else: for name in render_name: aovs_frames.update({ f"{camera}_{name}": self.get_expected_aovs( filename, name, start_frame, end_frame, ext) }) elif renderer == "Arnold": render_name = self.get_arnold_product_name() if render_name: for name in render_name: aovs_frames.update({ f"{camera}_{name}": self.get_expected_arnold_product( # noqa filename, name, start_frame, end_frame, ext) }) elif renderer in [ "V_Ray_6_Hotfix_3", "V_Ray_GPU_6_Hotfix_3" ]: if ext != "exr": render_name = self.get_render_elements_name() if render_name: for name in render_name: aovs_frames.update({ f"{camera}_{name}": self.get_expected_aovs( filename, name, start_frame, end_frame, ext) }) return aovs_frames def get_aovs(self, container): render_dir = os.path.dirname(rt.rendOutputFilename) output_file = os.path.join(render_dir, container) setting = self._project_settings img_fmt = setting["max"]["RenderSettings"]["image_format"] # noqa start_frame = int(rt.rendStart) end_frame = int(rt.rendEnd) + 1 renderer_class = get_current_renderer() renderer = str(renderer_class).split(":")[0] render_dict = {} if renderer in [ "ART_Renderer", "V_Ray_6_Hotfix_3", "V_Ray_GPU_6_Hotfix_3", "Default_Scanline_Renderer", "Quicksilver_Hardware_Renderer", ]: render_name = self.get_render_elements_name() if render_name: for name in render_name: render_dict.update({ name: self.get_expected_aovs( output_file, name, start_frame, end_frame, img_fmt) }) elif renderer == "Redshift_Renderer": render_name = self.get_render_elements_name() if render_name: rs_aov_files = rt.Execute("renderers.current.separateAovFiles") # this doesn't work, always returns False # rs_AovFiles = rt.RedShift_Renderer().separateAovFiles if img_fmt == "exr" and not rs_aov_files: for name in render_name: if name == "RsCryptomatte": render_dict.update({ name: self.get_expected_aovs( output_file, name, start_frame, end_frame, img_fmt) }) else: for name in render_name: render_dict.update({ name: self.get_expected_aovs( output_file, name, start_frame, end_frame, img_fmt) }) elif renderer == "Arnold": render_name = self.get_arnold_product_name() if render_name: for name in render_name: render_dict.update({ name: self.get_expected_arnold_product( output_file, name, start_frame, end_frame, img_fmt) }) elif renderer in [ "V_Ray_6_Hotfix_3", "V_Ray_GPU_6_Hotfix_3" ]: if img_fmt != "exr": render_name = self.get_render_elements_name() if render_name: for name in render_name: render_dict.update({ name: self.get_expected_aovs( output_file, name, start_frame, end_frame, img_fmt) # noqa }) return render_dict def get_expected_beauty(self, folder, start_frame, end_frame, fmt): beauty_frame_range = [] for f in range(start_frame, end_frame): frame = "%04d" % f beauty_output = f"{folder}.{frame}.{fmt}" beauty_output = beauty_output.replace("\\", "/") beauty_frame_range.append(beauty_output) return beauty_frame_range def get_arnold_product_name(self): """Get all the Arnold AOVs name""" aov_name = [] amw = rt.MaxToAOps.AOVsManagerWindow() aov_mgr = rt.renderers.current.AOVManager # Check if there is any aov group set in AOV manager aov_group_num = len(aov_mgr.drivers) if aov_group_num < 1: return for i in range(aov_group_num): # get the specific AOV group aov_name.extend(aov.name for aov in aov_mgr.drivers[i].aov_list) # close the AOVs manager window amw.close() return aov_name def get_expected_arnold_product(self, folder, name, start_frame, end_frame, fmt): """Get all the expected Arnold AOVs""" aov_list = [] for f in range(start_frame, end_frame): frame = "%04d" % f render_element = f"{folder}_{name}.{frame}.{fmt}" render_element = render_element.replace("\\", "/") aov_list.append(render_element) return aov_list def get_render_elements_name(self): """Get all the render element names for general """ render_name = [] render_elem = rt.maxOps.GetCurRenderElementMgr() render_elem_num = render_elem.NumRenderElements() if render_elem_num < 1: return # get render elements from the renders for i in range(render_elem_num): renderlayer_name = render_elem.GetRenderElement(i) if renderlayer_name.enabled: target, renderpass = str(renderlayer_name).split(":") render_name.append(renderpass) return render_name def get_expected_aovs(self, folder, name, start_frame, end_frame, fmt): """Get all the expected render element output files. """ render_elements = [] for f in range(start_frame, end_frame): frame = "%04d" % f render_element = f"{folder}_{name}.{frame}.{fmt}" render_element = render_element.replace("\\", "/") render_elements.append(render_element) return render_elements def image_format(self): return self._project_settings["max"]["RenderSettings"]["image_format"] # noqa ================================================ FILE: openpype/hosts/max/api/lib_rendersettings.py ================================================ import os from pymxs import runtime as rt from openpype.lib import Logger from openpype.settings import get_project_settings from openpype.pipeline import get_current_project_name from openpype.pipeline.context_tools import get_current_project_asset from openpype.hosts.max.api.lib import ( set_render_frame_range, get_current_renderer, get_default_render_folder ) class RenderSettings(object): log = Logger.get_logger("RenderSettings") _aov_chars = { "dot": ".", "dash": "-", "underscore": "_" } def __init__(self, project_settings=None): """ Set up the naming convention for the render elements for the deadline submission """ self._project_settings = project_settings if not self._project_settings: self._project_settings = get_project_settings( get_current_project_name() ) def set_render_camera(self, selection): for sel in selection: # to avoid Attribute Error from pymxs wrapper if rt.classOf(sel) in rt.Camera.classes: rt.viewport.setCamera(sel) return raise RuntimeError("Active Camera not found") def render_output(self, container): folder = rt.maxFilePath # hard-coded, should be customized in the setting file = rt.maxFileName folder = folder.replace("\\", "/") # hard-coded, set the renderoutput path setting = self._project_settings render_folder = get_default_render_folder(setting) filename, ext = os.path.splitext(file) output_dir = os.path.join(folder, render_folder, filename) if not os.path.exists(output_dir): os.makedirs(output_dir) # hard-coded, should be customized in the setting context = get_current_project_asset() # get project resolution width = context["data"].get("resolutionWidth") height = context["data"].get("resolutionHeight") # Set Frame Range frame_start = context["data"].get("frame_start") frame_end = context["data"].get("frame_end") set_render_frame_range(frame_start, frame_end) # get the production render renderer_class = get_current_renderer() renderer = str(renderer_class).split(":")[0] img_fmt = self._project_settings["max"]["RenderSettings"]["image_format"] # noqa output = os.path.join(output_dir, container) try: aov_separator = self._aov_chars[( self._project_settings["max"] ["RenderSettings"] ["aov_separator"] )] except KeyError: aov_separator = "." output_filename = f"{output}..{img_fmt}" output_filename = output_filename.replace("{aov_separator}", aov_separator) rt.rendOutputFilename = output_filename if renderer == "VUE_File_Renderer": return # TODO: Finish the arnold render setup if renderer == "Arnold": self.arnold_setup() if renderer in [ "ART_Renderer", "Redshift_Renderer", "V_Ray_6_Hotfix_3", "V_Ray_GPU_6_Hotfix_3", "Default_Scanline_Renderer", "Quicksilver_Hardware_Renderer", ]: self.render_element_layer(output, width, height, img_fmt) rt.rendSaveFile = True if rt.renderSceneDialog.isOpen(): rt.renderSceneDialog.close() def arnold_setup(self): # get Arnold RenderView run in the background # for setting up renderable camera arv = rt.MAXToAOps.ArnoldRenderView() render_camera = rt.viewport.GetCamera() if render_camera: arv.setOption("Camera", str(render_camera)) # TODO: add AOVs and extension img_fmt = self._project_settings["max"]["RenderSettings"]["image_format"] # noqa setup_cmd = ( f""" amw = MaxtoAOps.AOVsManagerWindow() amw.close() aovmgr = renderers.current.AOVManager aovmgr.drivers = #() img_fmt = "{img_fmt}" if img_fmt == "png" then driver = ArnoldPNGDriver() if img_fmt == "jpg" then driver = ArnoldJPEGDriver() if img_fmt == "exr" then driver = ArnoldEXRDriver() if img_fmt == "tif" then driver = ArnoldTIFFDriver() if img_fmt == "tiff" then driver = ArnoldTIFFDriver() append aovmgr.drivers driver aovmgr.drivers[1].aov_list = #() """) rt.execute(setup_cmd) arv.close() def render_element_layer(self, dir, width, height, ext): """For Renderers with render elements""" rt.renderWidth = width rt.renderHeight = height render_elem = rt.maxOps.GetCurRenderElementMgr() render_elem_num = render_elem.NumRenderElements() if render_elem_num < 0: return for i in range(render_elem_num): renderlayer_name = render_elem.GetRenderElement(i) target, renderpass = str(renderlayer_name).split(":") aov_name = f"{dir}_{renderpass}..{ext}" render_elem.SetRenderElementFileName(i, aov_name) def get_render_output(self, container, output_dir): output = os.path.join(output_dir, container) img_fmt = self._project_settings["max"]["RenderSettings"]["image_format"] # noqa output_filename = f"{output}..{img_fmt}" return output_filename def get_render_element(self): orig_render_elem = [] render_elem = rt.maxOps.GetCurRenderElementMgr() render_elem_num = render_elem.NumRenderElements() if render_elem_num < 0: return for i in range(render_elem_num): render_element = render_elem.GetRenderElementFilename(i) orig_render_elem.append(render_element) return orig_render_elem def get_batch_render_elements(self, container, output_dir, camera): render_element_list = list() output = os.path.join(output_dir, container) render_elem = rt.maxOps.GetCurRenderElementMgr() render_elem_num = render_elem.NumRenderElements() if render_elem_num < 0: return img_fmt = self._project_settings["max"]["RenderSettings"]["image_format"] # noqa for i in range(render_elem_num): renderlayer_name = render_elem.GetRenderElement(i) target, renderpass = str(renderlayer_name).split(":") aov_name = f"{output}_{camera}_{renderpass}..{img_fmt}" render_element_list.append(aov_name) return render_element_list def get_batch_render_output(self, camera): target_layer_no = rt.batchRenderMgr.FindView(camera) target_layer = rt.batchRenderMgr.GetView(target_layer_no) return target_layer.outputFilename def batch_render_elements(self, camera): target_layer_no = rt.batchRenderMgr.FindView(camera) target_layer = rt.batchRenderMgr.GetView(target_layer_no) outputfilename = target_layer.outputFilename directory = os.path.dirname(outputfilename) render_elem = rt.maxOps.GetCurRenderElementMgr() render_elem_num = render_elem.NumRenderElements() if render_elem_num < 0: return ext = self._project_settings["max"]["RenderSettings"]["image_format"] # noqa for i in range(render_elem_num): renderlayer_name = render_elem.GetRenderElement(i) target, renderpass = str(renderlayer_name).split(":") aov_name = f"{directory}_{camera}_{renderpass}..{ext}" render_elem.SetRenderElementFileName(i, aov_name) def batch_render_layer(self, container, output_dir, cameras): outputs = list() output = os.path.join(output_dir, container) img_fmt = self._project_settings["max"]["RenderSettings"]["image_format"] # noqa for cam in cameras: camera = rt.getNodeByName(cam) layer_no = rt.batchRenderMgr.FindView(cam) renderlayer = None if layer_no == 0: renderlayer = rt.batchRenderMgr.CreateView(camera) else: renderlayer = rt.batchRenderMgr.GetView(layer_no) # use camera name as renderlayer name renderlayer.name = cam renderlayer.outputFilename = f"{output}_{cam}..{img_fmt}" outputs.append(renderlayer.outputFilename) return outputs ================================================ FILE: openpype/hosts/max/api/menu.py ================================================ # -*- coding: utf-8 -*- """3dsmax menu definition of AYON.""" import os from qtpy import QtWidgets, QtCore from pymxs import runtime as rt from openpype.tools.utils import host_tools from openpype.hosts.max.api import lib class OpenPypeMenu(object): """Object representing OpenPype/AYON menu. This is using "hack" to inject itself before "Help" menu of 3dsmax. For some reason `postLoadingMenus` event doesn't fire, and main menu if probably re-initialized by menu templates, se we wait for at least 1 event Qt event loop before trying to insert. """ def __init__(self): super().__init__() self.main_widget = self.get_main_widget() self.menu = None timer = QtCore.QTimer() # set number of event loops to wait. timer.setInterval(1) timer.timeout.connect(self._on_timer) timer.start() self._timer = timer self._counter = 0 def _on_timer(self): if self._counter < 1: self._counter += 1 return self._counter = 0 self._timer.stop() self.build_openpype_menu() @staticmethod def get_main_widget(): """Get 3dsmax main window.""" return QtWidgets.QWidget.find(rt.windows.getMAXHWND()) def get_main_menubar(self) -> QtWidgets.QMenuBar: """Get main Menubar by 3dsmax main window.""" return list(self.main_widget.findChildren(QtWidgets.QMenuBar))[0] def get_or_create_openpype_menu( self, name: str = "&Openpype", before: str = "&Help") -> QtWidgets.QAction: """Create AYON menu. Args: name (str, Optional): AYON menu name. before (str, Optional): Name of the 3dsmax main menu item to add AYON menu before. Returns: QtWidgets.QAction: AYON menu action. """ if self.menu is not None: return self.menu menu_bar = self.get_main_menubar() menu_items = menu_bar.findChildren( QtWidgets.QMenu, options=QtCore.Qt.FindDirectChildrenOnly) help_action = None for item in menu_items: if name in item.title(): # we already have OpenPype menu return item if before in item.title(): help_action = item.menuAction() tab_menu_label = os.environ.get("AVALON_LABEL") or "AYON" op_menu = QtWidgets.QMenu("&{}".format(tab_menu_label)) menu_bar.insertMenu(help_action, op_menu) self.menu = op_menu return op_menu def build_openpype_menu(self) -> QtWidgets.QAction: """Build items in AYON menu.""" openpype_menu = self.get_or_create_openpype_menu() load_action = QtWidgets.QAction("Load...", openpype_menu) load_action.triggered.connect(self.load_callback) openpype_menu.addAction(load_action) publish_action = QtWidgets.QAction("Publish...", openpype_menu) publish_action.triggered.connect(self.publish_callback) openpype_menu.addAction(publish_action) manage_action = QtWidgets.QAction("Manage...", openpype_menu) manage_action.triggered.connect(self.manage_callback) openpype_menu.addAction(manage_action) library_action = QtWidgets.QAction("Library...", openpype_menu) library_action.triggered.connect(self.library_callback) openpype_menu.addAction(library_action) openpype_menu.addSeparator() workfiles_action = QtWidgets.QAction("Work Files...", openpype_menu) workfiles_action.triggered.connect(self.workfiles_callback) openpype_menu.addAction(workfiles_action) openpype_menu.addSeparator() res_action = QtWidgets.QAction("Set Resolution", openpype_menu) res_action.triggered.connect(self.resolution_callback) openpype_menu.addAction(res_action) frame_action = QtWidgets.QAction("Set Frame Range", openpype_menu) frame_action.triggered.connect(self.frame_range_callback) openpype_menu.addAction(frame_action) colorspace_action = QtWidgets.QAction("Set Colorspace", openpype_menu) colorspace_action.triggered.connect(self.colorspace_callback) openpype_menu.addAction(colorspace_action) unit_scale_action = QtWidgets.QAction("Set Unit Scale", openpype_menu) unit_scale_action.triggered.connect(self.unit_scale_callback) openpype_menu.addAction(unit_scale_action) return openpype_menu def load_callback(self): """Callback to show Loader tool.""" host_tools.show_loader(parent=self.main_widget) def publish_callback(self): """Callback to show Publisher tool.""" host_tools.show_publisher(parent=self.main_widget) def manage_callback(self): """Callback to show Scene Manager/Inventory tool.""" host_tools.show_scene_inventory(parent=self.main_widget) def library_callback(self): """Callback to show Library Loader tool.""" host_tools.show_library_loader(parent=self.main_widget) def workfiles_callback(self): """Callback to show Workfiles tool.""" host_tools.show_workfiles(parent=self.main_widget) def resolution_callback(self): """Callback to reset scene resolution""" return lib.reset_scene_resolution() def frame_range_callback(self): """Callback to reset frame range""" return lib.reset_frame_range() def colorspace_callback(self): """Callback to reset colorspace""" return lib.reset_colorspace() def unit_scale_callback(self): """Callback to reset unit scale""" return lib.reset_unit_scale() ================================================ FILE: openpype/hosts/max/api/pipeline.py ================================================ # -*- coding: utf-8 -*- """Pipeline tools for OpenPype Houdini integration.""" import os import logging from operator import attrgetter import json from openpype.host import HostBase, IWorkfileHost, ILoadHost, IPublishHost import pyblish.api from openpype.pipeline import ( register_creator_plugin_path, register_loader_plugin_path, AVALON_CONTAINER_ID, ) from openpype.hosts.max.api.menu import OpenPypeMenu from openpype.hosts.max.api import lib from openpype.hosts.max.api.plugin import MS_CUSTOM_ATTRIB from openpype.hosts.max import MAX_HOST_DIR from pymxs import runtime as rt # noqa log = logging.getLogger("openpype.hosts.max") PLUGINS_DIR = os.path.join(MAX_HOST_DIR, "plugins") PUBLISH_PATH = os.path.join(PLUGINS_DIR, "publish") LOAD_PATH = os.path.join(PLUGINS_DIR, "load") CREATE_PATH = os.path.join(PLUGINS_DIR, "create") INVENTORY_PATH = os.path.join(PLUGINS_DIR, "inventory") class MaxHost(HostBase, IWorkfileHost, ILoadHost, IPublishHost): name = "max" menu = None def __init__(self): super(MaxHost, self).__init__() self._op_events = {} self._has_been_setup = False def install(self): pyblish.api.register_host("max") pyblish.api.register_plugin_path(PUBLISH_PATH) register_loader_plugin_path(LOAD_PATH) register_creator_plugin_path(CREATE_PATH) # self._register_callbacks() self.menu = OpenPypeMenu() self._has_been_setup = True def context_setting(): return lib.set_context_setting() rt.callbacks.addScript(rt.Name('systemPostNew'), context_setting) rt.callbacks.addScript(rt.Name('filePostOpen'), lib.check_colorspace) rt.callbacks.addScript(rt.Name('postWorkspaceChange'), self._deferred_menu_creation) def workfile_has_unsaved_changes(self): return rt.getSaveRequired() def get_workfile_extensions(self): return [".max"] def save_workfile(self, dst_path=None): rt.saveMaxFile(dst_path) return dst_path def open_workfile(self, filepath): rt.checkForSave() rt.loadMaxFile(filepath) return filepath def get_current_workfile(self): return os.path.join(rt.maxFilePath, rt.maxFileName) def get_containers(self): return ls() def _register_callbacks(self): rt.callbacks.removeScripts(id=rt.name("OpenPypeCallbacks")) rt.callbacks.addScript( rt.Name("postLoadingMenus"), self._deferred_menu_creation, id=rt.Name('OpenPypeCallbacks')) def _deferred_menu_creation(self): self.log.info("Building menu ...") self.menu = OpenPypeMenu() @staticmethod def create_context_node(): """Helper for creating context holding node.""" root_scene = rt.rootScene create_attr_script = (""" attributes "OpenPypeContext" ( parameters main rollout:params ( context type: #string ) rollout params "OpenPype Parameters" ( editText editTextContext "Context" type: #string ) ) """) attr = rt.execute(create_attr_script) rt.custAttributes.add(root_scene, attr) return root_scene.OpenPypeContext.context def update_context_data(self, data, changes): try: _ = rt.rootScene.OpenPypeContext.context except AttributeError: # context node doesn't exists self.create_context_node() rt.rootScene.OpenPypeContext.context = json.dumps(data) def get_context_data(self): try: context = rt.rootScene.OpenPypeContext.context except AttributeError: # context node doesn't exists context = self.create_context_node() if not context: context = "{}" return json.loads(context) def save_file(self, dst_path=None): # Force forwards slashes to avoid segfault dst_path = dst_path.replace("\\", "/") rt.saveMaxFile(dst_path) def ls() -> list: """Get all OpenPype instances.""" objs = rt.objects containers = [ obj for obj in objs if rt.getUserProp(obj, "id") == AVALON_CONTAINER_ID ] for container in sorted(containers, key=attrgetter("name")): yield lib.read(container) def containerise(name: str, nodes: list, context, namespace=None, loader=None, suffix="_CON"): data = { "schema": "openpype:container-2.0", "id": AVALON_CONTAINER_ID, "name": name, "namespace": namespace or "", "loader": loader, "representation": context["representation"]["_id"], } container_name = f"{namespace}:{name}{suffix}" container = rt.container(name=container_name) import_custom_attribute_data(container, nodes) if not lib.imprint(container_name, data): print(f"imprinting of {container_name} failed.") return container def load_custom_attribute_data(): """Re-loading the AYON custom parameter built by the creator Returns: attribute: re-loading the custom OP attributes set in Maxscript """ return rt.Execute(MS_CUSTOM_ATTRIB) def import_custom_attribute_data(container: str, selections: list): """Importing the Openpype/AYON custom parameter built by the creator Args: container (str): target container which adds custom attributes selections (list): nodes to be added into group in custom attributes """ attrs = load_custom_attribute_data() modifier = rt.EmptyModifier() rt.addModifier(container, modifier) container.modifiers[0].name = "OP Data" rt.custAttributes.add(container.modifiers[0], attrs) node_list = [] sel_list = [] for i in selections: node_ref = rt.NodeTransformMonitor(node=i) node_list.append(node_ref) sel_list.append(str(i)) # Setting the property rt.setProperty( container.modifiers[0].openPypeData, "all_handles", node_list) rt.setProperty( container.modifiers[0].openPypeData, "sel_list", sel_list) def update_custom_attribute_data(container: str, selections: list): """Updating the AYON custom parameter built by the creator Args: container (str): target container which adds custom attributes selections (list): nodes to be added into group in custom attributes """ if container.modifiers[0].name == "OP Data": rt.deleteModifier(container, container.modifiers[0]) import_custom_attribute_data(container, selections) def get_previous_loaded_object(container: str): """Get previous loaded_object through the OP data Args: container (str): the container which stores the OP data Returns: node_list(list): list of nodes which are previously loaded """ node_list = [] sel_list = rt.getProperty(container.modifiers[0].openPypeData, "sel_list") for obj in rt.Objects: if str(obj) in sel_list: node_list.append(obj) return node_list ================================================ FILE: openpype/hosts/max/api/plugin.py ================================================ # -*- coding: utf-8 -*- """3dsmax specific Avalon/Pyblish plugin definitions.""" from abc import ABCMeta import six from pymxs import runtime as rt from openpype.lib import BoolDef from openpype.pipeline import CreatedInstance, Creator, CreatorError from .lib import imprint, lsattr, read MS_CUSTOM_ATTRIB = """attributes "openPypeData" ( parameters main rollout:OPparams ( all_handles type:#maxObjectTab tabSize:0 tabSizeVariable:on sel_list type:#stringTab tabSize:0 tabSizeVariable:on ) rollout OPparams "OP Parameters" ( listbox list_node "Node References" items:#() button button_add "Add to Container" button button_del "Delete from Container" fn node_to_name the_node = ( handle = the_node.handle obj_name = the_node.name handle_name = obj_name + "<" + handle as string + ">" return handle_name ) fn nodes_to_add node = ( sceneObjs = #() if classOf node == Container do return false n = node as string for obj in Objects do ( tmp_obj = obj as string append sceneObjs tmp_obj ) if sel_list != undefined do ( for obj in sel_list do ( idx = findItem sceneObjs obj if idx do ( deleteItem sceneObjs idx ) ) ) idx = findItem sceneObjs n if idx then return true else false ) fn nodes_to_rmv node = ( n = node as string idx = findItem sel_list n if idx then return true else false ) on button_add pressed do ( current_sel = selectByName title:"Select Objects to add to the Container" buttontext:"Add" filter:nodes_to_add if current_sel == undefined then return False temp_arr = #() i_node_arr = #() for c in current_sel do ( handle_name = node_to_name c node_ref = NodeTransformMonitor node:c idx = finditem list_node.items handle_name if idx do ( continue ) name = c as string append temp_arr handle_name append i_node_arr node_ref append sel_list name ) all_handles = join i_node_arr all_handles list_node.items = join temp_arr list_node.items ) on button_del pressed do ( current_sel = selectByName title:"Select Objects to remove from the Container" buttontext:"Remove" filter: nodes_to_rmv if current_sel == undefined or current_sel.count == 0 then ( return False ) temp_arr = #() i_node_arr = #() new_i_node_arr = #() new_temp_arr = #() for c in current_sel do ( node_ref = NodeTransformMonitor node:c as string handle_name = node_to_name c n = c as string tmp_all_handles = #() for i in all_handles do ( tmp = i as string append tmp_all_handles tmp ) idx = finditem tmp_all_handles node_ref if idx do ( new_i_node_arr = DeleteItem all_handles idx ) idx = finditem list_node.items handle_name if idx do ( new_temp_arr = DeleteItem list_node.items idx ) idx = finditem sel_list n if idx do ( sel_list = DeleteItem sel_list idx ) ) all_handles = join i_node_arr new_i_node_arr list_node.items = join temp_arr new_temp_arr ) on OPparams open do ( if all_handles.count != 0 then ( temp_arr = #() for x in all_handles do ( if x.node == undefined do continue handle_name = node_to_name x.node append temp_arr handle_name ) list_node.items = temp_arr ) ) ) )""" class OpenPypeCreatorError(CreatorError): pass class MaxCreatorBase(object): @staticmethod def cache_subsets(shared_data): if shared_data.get("max_cached_subsets") is not None: return shared_data shared_data["max_cached_subsets"] = {} cached_instances = lsattr("id", "pyblish.avalon.instance") for i in cached_instances: creator_id = rt.GetUserProp(i, "creator_identifier") if creator_id not in shared_data["max_cached_subsets"]: shared_data["max_cached_subsets"][creator_id] = [i.name] else: shared_data[ "max_cached_subsets"][creator_id].append(i.name) return shared_data @staticmethod def create_instance_node(node): """Create instance node. If the supplied node is existing node, it will be used to hold the instance, otherwise new node of type Dummy will be created. Args: node (rt.MXSWrapperBase, str): Node or node name to use. Returns: instance """ if isinstance(node, str): node = rt.Container(name=node) attrs = rt.Execute(MS_CUSTOM_ATTRIB) modifier = rt.EmptyModifier() rt.addModifier(node, modifier) node.modifiers[0].name = "OP Data" rt.custAttributes.add(node.modifiers[0], attrs) return node @six.add_metaclass(ABCMeta) class MaxCreator(Creator, MaxCreatorBase): selected_nodes = [] def create(self, subset_name, instance_data, pre_create_data): if pre_create_data.get("use_selection"): self.selected_nodes = rt.GetCurrentSelection() if rt.getNodeByName(subset_name): raise CreatorError(f"'{subset_name}' is already created..") instance_node = self.create_instance_node(subset_name) instance_data["instance_node"] = instance_node.name instance = CreatedInstance( self.family, subset_name, instance_data, self ) if pre_create_data.get("use_selection"): node_list = [] sel_list = [] for i in self.selected_nodes: node_ref = rt.NodeTransformMonitor(node=i) node_list.append(node_ref) sel_list.append(str(i)) # Setting the property rt.setProperty( instance_node.modifiers[0].openPypeData, "all_handles", node_list) rt.setProperty( instance_node.modifiers[0].openPypeData, "sel_list", sel_list) self._add_instance_to_context(instance) imprint(instance_node.name, instance.data_to_store()) return instance def collect_instances(self): self.cache_subsets(self.collection_shared_data) for instance in self.collection_shared_data["max_cached_subsets"].get(self.identifier, []): # noqa created_instance = CreatedInstance.from_existing( read(rt.GetNodeByName(instance)), self ) self._add_instance_to_context(created_instance) def update_instances(self, update_list): for created_inst, changes in update_list: instance_node = created_inst.get("instance_node") new_values = { key: changes[key].new_value for key in changes.changed_keys } subset = new_values.get("subset", "") if subset and instance_node != subset: node = rt.getNodeByName(instance_node) new_subset_name = new_values["subset"] if rt.getNodeByName(new_subset_name): raise CreatorError( "The subset '{}' already exists.".format( new_subset_name)) instance_node = new_subset_name created_inst["instance_node"] = instance_node node.name = instance_node imprint( instance_node, created_inst.data_to_store(), ) def remove_instances(self, instances): """Remove specified instance from the scene. This is only removing `id` parameter so instance is no longer instance, because it might contain valuable data for artist. """ for instance in instances: instance_node = rt.GetNodeByName( instance.data.get("instance_node")) if instance_node: count = rt.custAttributes.count(instance_node.modifiers[0]) rt.custAttributes.delete(instance_node.modifiers[0], count) rt.Delete(instance_node) self._remove_instance_from_context(instance) def get_pre_create_attr_defs(self): return [ BoolDef("use_selection", label="Use selection") ] ================================================ FILE: openpype/hosts/max/api/preview_animation.py ================================================ import logging import contextlib from pymxs import runtime as rt from .lib import get_max_version, render_resolution log = logging.getLogger("openpype.hosts.max") @contextlib.contextmanager def play_preview_when_done(has_autoplay): """Set preview playback option during context Args: has_autoplay (bool): autoplay during creating preview animation """ current_playback = rt.preferences.playPreviewWhenDone try: rt.preferences.playPreviewWhenDone = has_autoplay yield finally: rt.preferences.playPreviewWhenDone = current_playback @contextlib.contextmanager def viewport_layout_and_camera(camera, layout="layout_1"): """Set viewport layout and camera during context ***For 3dsMax 2024+ Args: camera (str): viewport camera layout (str): layout to use in viewport, defaults to `layout_1` Use None to not change viewport layout during context. """ original_camera = rt.viewport.getCamera() original_layout = rt.viewport.getLayout() if not original_camera: # if there is no original camera # use the current camera as original original_camera = rt.getNodeByName(camera) review_camera = rt.getNodeByName(camera) try: if layout is not None: layout = rt.Name(layout) if rt.viewport.getLayout() != layout: rt.viewport.setLayout(layout) rt.viewport.setCamera(review_camera) yield finally: rt.viewport.setLayout(original_layout) rt.viewport.setCamera(original_camera) @contextlib.contextmanager def viewport_preference_setting(general_viewport, nitrous_manager, nitrous_viewport, vp_button_mgr): """Function to set viewport setting during context ***For Max Version < 2024 Args: camera (str): Viewport camera for review render general_viewport (dict): General viewport setting nitrous_manager (dict): Nitrous graphic manager nitrous_viewport (dict): Nitrous setting for preview animation vp_button_mgr (dict): Viewport button manager Setting preview_preferences (dict): Preview Preferences Setting """ orig_vp_grid = rt.viewport.getGridVisibility(1) orig_vp_bkg = rt.viewport.IsSolidBackgroundColorMode() nitrousGraphicMgr = rt.NitrousGraphicsManager viewport_setting = nitrousGraphicMgr.GetActiveViewportSetting() vp_button_mgr_original = { key: getattr(rt.ViewportButtonMgr, key) for key in vp_button_mgr } nitrous_manager_original = { key: getattr(nitrousGraphicMgr, key) for key in nitrous_manager } nitrous_viewport_original = { key: getattr(viewport_setting, key) for key in nitrous_viewport } try: rt.viewport.setGridVisibility(1, general_viewport["dspGrid"]) rt.viewport.EnableSolidBackgroundColorMode(general_viewport["dspBkg"]) for key, value in vp_button_mgr.items(): setattr(rt.ViewportButtonMgr, key, value) for key, value in nitrous_manager.items(): setattr(nitrousGraphicMgr, key, value) for key, value in nitrous_viewport.items(): if nitrous_viewport[key] != nitrous_viewport_original[key]: setattr(viewport_setting, key, value) yield finally: rt.viewport.setGridVisibility(1, orig_vp_grid) rt.viewport.EnableSolidBackgroundColorMode(orig_vp_bkg) for key, value in vp_button_mgr_original.items(): setattr(rt.ViewportButtonMgr, key, value) for key, value in nitrous_manager_original.items(): setattr(nitrousGraphicMgr, key, value) for key, value in nitrous_viewport_original.items(): setattr(viewport_setting, key, value) def _render_preview_animation_max_2024( filepath, start, end, percentSize, ext, viewport_options): """Render viewport preview with MaxScript using `CreateAnimation`. ****For 3dsMax 2024+ Args: filepath (str): filepath for render output without frame number and extension, for example: /path/to/file start (int): startFrame end (int): endFrame percentSize (float): render resolution multiplier by 100 e.g. 100.0 is 1x, 50.0 is 0.5x, 150.0 is 1.5x viewport_options (dict): viewport setting options, e.g. {"vpStyle": "defaultshading", "vpPreset": "highquality"} Returns: list: Created files """ # the percentSize argument must be integer percent = int(percentSize) filepath = filepath.replace("\\", "/") preview_output = f"{filepath}..{ext}" frame_template = f"{filepath}.{{:04d}}.{ext}" job_args = [] for key, value in viewport_options.items(): if isinstance(value, bool): if value: job_args.append(f"{key}:{value}") elif isinstance(value, str): if key == "vpStyle": if value == "Realistic": value = "defaultshading" elif value == "Shaded": log.warning( "'Shaded' Mode not supported in " "preview animation in Max 2024.\n" "Using 'defaultshading' instead.") value = "defaultshading" elif value == "ConsistentColors": value = "flatcolor" else: value = value.lower() elif key == "vpPreset": if value == "Quality": value = "highquality" elif value == "Customize": value = "userdefined" else: value = value.lower() job_args.append(f"{key}: #{value}") job_str = ( f'CreatePreview filename:"{preview_output}" outputAVI:false ' f"percentSize:{percent} start:{start} end:{end} " f"{' '.join(job_args)} " "autoPlay:false" ) rt.completeRedraw() rt.execute(job_str) # Return the created files return [frame_template.format(frame) for frame in range(start, end + 1)] def _render_preview_animation_max_pre_2024( filepath, startFrame, endFrame, width, height, percentSize, ext): """Render viewport animation by creating bitmaps ***For 3dsMax Version <2024 Args: filepath (str): filepath without frame numbers and extension startFrame (int): start frame endFrame (int): end frame width (int): render resolution width height (int): render resolution height percentSize (float): render resolution multiplier by 100 e.g. 100.0 is 1x, 50.0 is 0.5x, 150.0 is 1.5x ext (str): image extension Returns: list: Created filepaths """ # get the screenshot percent = percentSize / 100.0 res_width = width * percent res_height = height * percent frame_template = "{}.{{:04}}.{}".format(filepath, ext) frame_template.replace("\\", "/") files = [] user_cancelled = False for frame in range(startFrame, endFrame + 1): rt.sliderTime = frame filepath = frame_template.format(frame) preview_res = rt.bitmap( res_width, res_height, filename=filepath ) dib = rt.gw.getViewportDib() dib_width = float(dib.width) dib_height = float(dib.height) # aspect ratio viewportRatio = dib_width / dib_height renderRatio = float(res_width / res_height) if viewportRatio < renderRatio: heightCrop = (dib_width / renderRatio) topEdge = int((dib_height - heightCrop) / 2.0) tempImage_bmp = rt.bitmap(dib_width, heightCrop) src_box_value = rt.Box2(0, topEdge, dib_width, heightCrop) rt.pasteBitmap(dib, tempImage_bmp, src_box_value, rt.Point2(0, 0)) rt.copy(tempImage_bmp, preview_res) rt.close(tempImage_bmp) elif viewportRatio > renderRatio: widthCrop = dib_height * renderRatio leftEdge = int((dib_width - widthCrop) / 2.0) tempImage_bmp = rt.bitmap(widthCrop, dib_height) src_box_value = rt.Box2(leftEdge, 0, widthCrop, dib_height) rt.pasteBitmap(dib, tempImage_bmp, src_box_value, rt.Point2(0, 0)) rt.copy(tempImage_bmp, preview_res) rt.close(tempImage_bmp) else: rt.copy(dib, preview_res) rt.save(preview_res) rt.close(preview_res) rt.close(dib) files.append(filepath) if rt.keyboard.escPressed: user_cancelled = True break # clean up the cache rt.gc(delayed=True) if user_cancelled: raise RuntimeError("User cancelled rendering of viewport animation.") return files def render_preview_animation( filepath, ext, camera, start_frame=None, end_frame=None, percentSize=100.0, width=1920, height=1080, viewport_options=None): """Render camera review animation Args: filepath (str): filepath to render to, without frame number and extension ext (str): output file extension camera (str): viewport camera for preview render start_frame (int): start frame end_frame (int): end frame percentSize (float): render resolution multiplier by 100 e.g. 100.0 is 1x, 50.0 is 0.5x, 150.0 is 1.5x width (int): render resolution width height (int): render resolution height viewport_options (dict): viewport setting options Returns: list: Rendered output files """ if start_frame is None: start_frame = int(rt.animationRange.start) if end_frame is None: end_frame = int(rt.animationRange.end) if viewport_options is None: viewport_options = viewport_options_for_preview_animation() with play_preview_when_done(False): with viewport_layout_and_camera(camera): if int(get_max_version()) < 2024: with viewport_preference_setting( viewport_options["general_viewport"], viewport_options["nitrous_manager"], viewport_options["nitrous_viewport"], viewport_options["vp_btn_mgr"] ): return _render_preview_animation_max_pre_2024( filepath, start_frame, end_frame, width, height, percentSize, ext ) else: with render_resolution(width, height): return _render_preview_animation_max_2024( filepath, start_frame, end_frame, percentSize, ext, viewport_options ) def viewport_options_for_preview_animation(): """Get default viewport options for `render_preview_animation`. Returns: dict: viewport setting options """ # viewport_options should be the dictionary if int(get_max_version()) < 2024: return { "visualStyleMode": "defaultshading", "viewportPreset": "highquality", "vpTexture": False, "dspGeometry": True, "dspShapes": False, "dspLights": False, "dspCameras": False, "dspHelpers": False, "dspParticles": True, "dspBones": False, "dspBkg": True, "dspGrid": False, "dspSafeFrame": False, "dspFrameNums": False } else: viewport_options = {} viewport_options["general_viewport"] = { "dspBkg": True, "dspGrid": False } viewport_options["nitrous_manager"] = { "AntialiasingQuality": "None" } viewport_options["nitrous_viewport"] = { "VisualStyleMode": "defaultshading", "ViewportPreset": "highquality", "UseTextureEnabled": False } viewport_options["vp_btn_mgr"] = { "EnableButtons": False} return viewport_options ================================================ FILE: openpype/hosts/max/hooks/force_startup_script.py ================================================ # -*- coding: utf-8 -*- """Pre-launch to force 3ds max startup script.""" import os from openpype.hosts.max import MAX_HOST_DIR from openpype.lib.applications import PreLaunchHook, LaunchTypes class ForceStartupScript(PreLaunchHook): """Inject OpenPype environment to 3ds max. Note that this works in combination whit 3dsmax startup script that is translating it back to PYTHONPATH for cases when 3dsmax drops PYTHONPATH environment. Hook `GlobalHostDataHook` must be executed before this hook. """ app_groups = {"3dsmax", "adsk_3dsmax"} order = 11 launch_types = {LaunchTypes.local} def execute(self): startup_args = [ "-U", "MAXScript", os.path.join(MAX_HOST_DIR, "startup", "startup.ms"), ] self.launch_context.launch_args.append(startup_args) ================================================ FILE: openpype/hosts/max/hooks/inject_python.py ================================================ # -*- coding: utf-8 -*- """Pre-launch hook to inject python environment.""" import os from openpype.lib.applications import PreLaunchHook, LaunchTypes class InjectPythonPath(PreLaunchHook): """Inject OpenPype environment to 3dsmax. Note that this works in combination whit 3dsmax startup script that is translating it back to PYTHONPATH for cases when 3dsmax drops PYTHONPATH environment. Hook `GlobalHostDataHook` must be executed before this hook. """ app_groups = {"3dsmax", "adsk_3dsmax"} launch_types = {LaunchTypes.local} def execute(self): self.launch_context.env["MAX_PYTHONPATH"] = os.environ["PYTHONPATH"] ================================================ FILE: openpype/hosts/max/hooks/set_paths.py ================================================ from openpype.lib.applications import PreLaunchHook, LaunchTypes class SetPath(PreLaunchHook): """Set current dir to workdir. Hook `GlobalHostDataHook` must be executed before this hook. """ app_groups = {"max"} launch_types = {LaunchTypes.local} def execute(self): workdir = self.launch_context.env.get("AVALON_WORKDIR", "") if not workdir: self.log.warning("BUG: Workdir is not filled.") return self.launch_context.kwargs["cwd"] = workdir ================================================ FILE: openpype/hosts/max/plugins/__init__.py ================================================ ================================================ FILE: openpype/hosts/max/plugins/create/create_camera.py ================================================ # -*- coding: utf-8 -*- """Creator plugin for creating camera.""" from openpype.hosts.max.api import plugin class CreateCamera(plugin.MaxCreator): """Creator plugin for Camera.""" identifier = "io.openpype.creators.max.camera" label = "Camera" family = "camera" icon = "gear" ================================================ FILE: openpype/hosts/max/plugins/create/create_maxScene.py ================================================ # -*- coding: utf-8 -*- """Creator plugin for creating raw max scene.""" from openpype.hosts.max.api import plugin class CreateMaxScene(plugin.MaxCreator): """Creator plugin for 3ds max scenes.""" identifier = "io.openpype.creators.max.maxScene" label = "Max Scene" family = "maxScene" icon = "gear" ================================================ FILE: openpype/hosts/max/plugins/create/create_model.py ================================================ # -*- coding: utf-8 -*- """Creator plugin for model.""" from openpype.hosts.max.api import plugin class CreateModel(plugin.MaxCreator): """Creator plugin for Model.""" identifier = "io.openpype.creators.max.model" label = "Model" family = "model" icon = "gear" ================================================ FILE: openpype/hosts/max/plugins/create/create_pointcache.py ================================================ # -*- coding: utf-8 -*- """Creator plugin for creating pointcache alembics.""" from openpype.hosts.max.api import plugin class CreatePointCache(plugin.MaxCreator): """Creator plugin for Point caches.""" identifier = "io.openpype.creators.max.pointcache" label = "Point Cache" family = "pointcache" icon = "gear" ================================================ FILE: openpype/hosts/max/plugins/create/create_pointcloud.py ================================================ # -*- coding: utf-8 -*- """Creator plugin for creating point cloud.""" from openpype.hosts.max.api import plugin class CreatePointCloud(plugin.MaxCreator): """Creator plugin for Point Clouds.""" identifier = "io.openpype.creators.max.pointcloud" label = "Point Cloud" family = "pointcloud" icon = "gear" ================================================ FILE: openpype/hosts/max/plugins/create/create_redshift_proxy.py ================================================ # -*- coding: utf-8 -*- """Creator plugin for creating camera.""" from openpype.hosts.max.api import plugin from openpype.pipeline import CreatedInstance class CreateRedshiftProxy(plugin.MaxCreator): identifier = "io.openpype.creators.max.redshiftproxy" label = "Redshift Proxy" family = "redshiftproxy" icon = "gear" ================================================ FILE: openpype/hosts/max/plugins/create/create_render.py ================================================ # -*- coding: utf-8 -*- """Creator plugin for creating camera.""" import os from openpype.hosts.max.api import plugin from openpype.lib import BoolDef from openpype.hosts.max.api.lib_rendersettings import RenderSettings class CreateRender(plugin.MaxCreator): """Creator plugin for Renders.""" identifier = "io.openpype.creators.max.render" label = "Render" family = "maxrender" icon = "gear" def create(self, subset_name, instance_data, pre_create_data): from pymxs import runtime as rt file = rt.maxFileName filename, _ = os.path.splitext(file) instance_data["AssetName"] = filename instance_data["multiCamera"] = pre_create_data.get("multi_cam") num_of_renderlayer = rt.batchRenderMgr.numViews if num_of_renderlayer > 0: rt.batchRenderMgr.DeleteView(num_of_renderlayer) instance = super(CreateRender, self).create( subset_name, instance_data, pre_create_data) container_name = instance.data.get("instance_node") # set output paths for rendering(mandatory for deadline) RenderSettings().render_output(container_name) # TODO: create multiple camera options if self.selected_nodes: selected_nodes_name = [] for sel in self.selected_nodes: name = sel.name selected_nodes_name.append(name) RenderSettings().batch_render_layer( container_name, filename, selected_nodes_name) def get_pre_create_attr_defs(self): attrs = super(CreateRender, self).get_pre_create_attr_defs() return attrs + [ BoolDef("multi_cam", label="Multiple Cameras Submission", default=False), ] ================================================ FILE: openpype/hosts/max/plugins/create/create_review.py ================================================ # -*- coding: utf-8 -*- """Creator plugin for creating review in Max.""" from openpype.hosts.max.api import plugin from openpype.lib import BoolDef, EnumDef, NumberDef class CreateReview(plugin.MaxCreator): """Review in 3dsMax""" identifier = "io.openpype.creators.max.review" label = "Review" family = "review" icon = "video-camera" review_width = 1920 review_height = 1080 percentSize = 100 keep_images = False image_format = "png" visual_style = "Realistic" viewport_preset = "Quality" vp_texture = True anti_aliasing = "None" def apply_settings(self, project_settings): settings = project_settings["max"]["CreateReview"] # noqa # Take some defaults from settings self.review_width = settings.get("review_width", self.review_width) self.review_height = settings.get("review_height", self.review_height) self.percentSize = settings.get("percentSize", self.percentSize) self.keep_images = settings.get("keep_images", self.keep_images) self.image_format = settings.get("image_format", self.image_format) self.visual_style = settings.get("visual_style", self.visual_style) self.viewport_preset = settings.get( "viewport_preset", self.viewport_preset) self.anti_aliasing = settings.get( "anti_aliasing", self.anti_aliasing) self.vp_texture = settings.get("vp_texture", self.vp_texture) def create(self, subset_name, instance_data, pre_create_data): # Transfer settings from pre create to instance creator_attributes = instance_data.setdefault( "creator_attributes", dict()) for key in ["imageFormat", "keepImages", "review_width", "review_height", "percentSize", "visualStyleMode", "viewportPreset", "antialiasingQuality", "vpTexture"]: if key in pre_create_data: creator_attributes[key] = pre_create_data[key] super(CreateReview, self).create( subset_name, instance_data, pre_create_data) def get_instance_attr_defs(self): image_format_enum = ["exr", "jpg", "png", "tga"] visual_style_preset_enum = [ "Realistic", "Shaded", "Facets", "ConsistentColors", "HiddenLine", "Wireframe", "BoundingBox", "Ink", "ColorInk", "Acrylic", "Tech", "Graphite", "ColorPencil", "Pastel", "Clay", "ModelAssist" ] preview_preset_enum = [ "Quality", "Standard", "Performance", "DXMode", "Customize"] anti_aliasing_enum = ["None", "2X", "4X", "8X"] return [ NumberDef("review_width", label="Review width", decimals=0, minimum=0, default=self.review_width), NumberDef("review_height", label="Review height", decimals=0, minimum=0, default=self.review_height), NumberDef("percentSize", label="Percent of Output", default=self.percentSize, minimum=1, decimals=0), BoolDef("keepImages", label="Keep Image Sequences", default=self.keep_images), EnumDef("imageFormat", image_format_enum, default=self.image_format, label="Image Format Options"), EnumDef("visualStyleMode", visual_style_preset_enum, default=self.visual_style, label="Preference"), EnumDef("viewportPreset", preview_preset_enum, default=self.viewport_preset, label="Preview Preset"), EnumDef("antialiasingQuality", anti_aliasing_enum, default=self.anti_aliasing, label="Anti-aliasing Quality"), BoolDef("vpTexture", label="Viewport Texture", default=self.vp_texture) ] def get_pre_create_attr_defs(self): # Use same attributes as for instance attributes attrs = super().get_pre_create_attr_defs() return attrs + self.get_instance_attr_defs() ================================================ FILE: openpype/hosts/max/plugins/create/create_tycache.py ================================================ # -*- coding: utf-8 -*- """Creator plugin for creating TyCache.""" from openpype.hosts.max.api import plugin class CreateTyCache(plugin.MaxCreator): """Creator plugin for TyCache.""" identifier = "io.openpype.creators.max.tycache" label = "TyCache" family = "tycache" icon = "gear" ================================================ FILE: openpype/hosts/max/plugins/create/create_workfile.py ================================================ # -*- coding: utf-8 -*- """Creator plugin for creating workfiles.""" from openpype import AYON_SERVER_ENABLED from openpype.pipeline import CreatedInstance, AutoCreator from openpype.client import get_asset_by_name, get_asset_name_identifier from openpype.hosts.max.api import plugin from openpype.hosts.max.api.lib import read, imprint from pymxs import runtime as rt class CreateWorkfile(plugin.MaxCreatorBase, AutoCreator): """Workfile auto-creator.""" identifier = "io.openpype.creators.max.workfile" label = "Workfile" family = "workfile" icon = "fa5.file" default_variant = "Main" def create(self): variant = self.default_variant current_instance = next( ( instance for instance in self.create_context.instances if instance.creator_identifier == self.identifier ), None) project_name = self.project_name asset_name = self.create_context.get_current_asset_name() task_name = self.create_context.get_current_task_name() host_name = self.create_context.host_name if current_instance is None: current_instance_asset = None elif AYON_SERVER_ENABLED: current_instance_asset = current_instance["folderPath"] else: current_instance_asset = current_instance["asset"] if current_instance is None: asset_doc = get_asset_by_name(project_name, asset_name) subset_name = self.get_subset_name( variant, task_name, asset_doc, project_name, host_name ) data = { "task": task_name, "variant": variant } if AYON_SERVER_ENABLED: data["folderPath"] = asset_name else: data["asset"] = asset_name data.update( self.get_dynamic_data( variant, task_name, asset_doc, project_name, host_name, current_instance) ) self.log.info("Auto-creating workfile instance...") instance_node = self.create_node(subset_name) data["instance_node"] = instance_node.name current_instance = CreatedInstance( self.family, subset_name, data, self ) self._add_instance_to_context(current_instance) imprint(instance_node.name, current_instance.data) elif ( current_instance_asset != asset_name or current_instance["task"] != task_name ): # Update instance context if is not the same asset_doc = get_asset_by_name(project_name, asset_name) subset_name = self.get_subset_name( variant, task_name, asset_doc, project_name, host_name ) asset_name = get_asset_name_identifier(asset_doc) if AYON_SERVER_ENABLED: current_instance["folderPath"] = asset_name else: current_instance["asset"] = asset_name current_instance["task"] = task_name current_instance["subset"] = subset_name def collect_instances(self): self.cache_subsets(self.collection_shared_data) for instance in self.collection_shared_data["max_cached_subsets"].get(self.identifier, []): # noqa if not rt.getNodeByName(instance): continue created_instance = CreatedInstance.from_existing( read(rt.GetNodeByName(instance)), self ) self._add_instance_to_context(created_instance) def update_instances(self, update_list): for created_inst, _ in update_list: instance_node = created_inst.get("instance_node") imprint( instance_node, created_inst.data_to_store() ) def remove_instances(self, instances): """Remove specified instance from the scene. This is only removing `id` parameter so instance is no longer instance, because it might contain valuable data for artist. """ for instance in instances: instance_node = rt.GetNodeByName( instance.data.get("instance_node")) if instance_node: rt.Delete(instance_node) self._remove_instance_from_context(instance) def create_node(self, subset_name): if rt.getNodeByName(subset_name): node = rt.getNodeByName(subset_name) return node node = rt.Container(name=subset_name) node.isHidden = True return node ================================================ FILE: openpype/hosts/max/plugins/load/load_camera_fbx.py ================================================ import os from openpype.hosts.max.api import lib, maintained_selection from openpype.hosts.max.api.lib import ( unique_namespace, get_namespace, object_transform_set ) from openpype.hosts.max.api.pipeline import ( containerise, get_previous_loaded_object, update_custom_attribute_data ) from openpype.pipeline import get_representation_path, load class FbxLoader(load.LoaderPlugin): """Fbx Loader.""" families = ["camera"] representations = ["fbx"] order = -9 icon = "code-fork" color = "white" def load(self, context, name=None, namespace=None, data=None): from pymxs import runtime as rt filepath = self.filepath_from_context(context) filepath = os.path.normpath(filepath) rt.FBXImporterSetParam("Animation", True) rt.FBXImporterSetParam("Camera", True) rt.FBXImporterSetParam("AxisConversionMethod", True) rt.FBXImporterSetParam("Mode", rt.Name("create")) rt.FBXImporterSetParam("Preserveinstances", True) rt.ImportFile( filepath, rt.name("noPrompt"), using=rt.FBXIMP) namespace = unique_namespace( name + "_", suffix="_", ) selections = rt.GetCurrentSelection() for selection in selections: selection.name = f"{namespace}:{selection.name}" return containerise( name, selections, context, namespace, loader=self.__class__.__name__) def update(self, container, representation): from pymxs import runtime as rt path = get_representation_path(representation) node_name = container["instance_node"] node = rt.getNodeByName(node_name) namespace, _ = get_namespace(node_name) node_list = get_previous_loaded_object(node) rt.Select(node_list) prev_fbx_objects = rt.GetCurrentSelection() transform_data = object_transform_set(prev_fbx_objects) for prev_fbx_obj in prev_fbx_objects: if rt.isValidNode(prev_fbx_obj): rt.Delete(prev_fbx_obj) rt.FBXImporterSetParam("Animation", True) rt.FBXImporterSetParam("Camera", True) rt.FBXImporterSetParam("Mode", rt.Name("merge")) rt.FBXImporterSetParam("AxisConversionMethod", True) rt.FBXImporterSetParam("Preserveinstances", True) rt.ImportFile( path, rt.name("noPrompt"), using=rt.FBXIMP) current_fbx_objects = rt.GetCurrentSelection() fbx_objects = [] for fbx_object in current_fbx_objects: fbx_object.name = f"{namespace}:{fbx_object.name}" fbx_objects.append(fbx_object) fbx_transform = f"{fbx_object.name}.transform" if fbx_transform in transform_data.keys(): fbx_object.pos = transform_data[fbx_transform] or 0 fbx_object.scale = transform_data[ f"{fbx_object.name}.scale"] or 0 update_custom_attribute_data(node, fbx_objects) lib.imprint(container["instance_node"], { "representation": str(representation["_id"]) }) def switch(self, container, representation): self.update(container, representation) def remove(self, container): from pymxs import runtime as rt node = rt.GetNodeByName(container["instance_node"]) rt.Delete(node) ================================================ FILE: openpype/hosts/max/plugins/load/load_max_scene.py ================================================ import os from openpype.hosts.max.api import lib from openpype.hosts.max.api.lib import ( unique_namespace, get_namespace, object_transform_set ) from openpype.hosts.max.api.pipeline import ( containerise, get_previous_loaded_object, update_custom_attribute_data ) from openpype.pipeline import get_representation_path, load class MaxSceneLoader(load.LoaderPlugin): """Max Scene Loader.""" families = ["camera", "maxScene", "model"] representations = ["max"] order = -8 icon = "code-fork" color = "green" def load(self, context, name=None, namespace=None, data=None): from pymxs import runtime as rt path = self.filepath_from_context(context) path = os.path.normpath(path) # import the max scene by using "merge file" path = path.replace('\\', '/') rt.MergeMaxFile(path, quiet=True, includeFullGroup=True) max_objects = rt.getLastMergedNodes() max_object_names = [obj.name for obj in max_objects] # implement the OP/AYON custom attributes before load max_container = [] namespace = unique_namespace( name + "_", suffix="_", ) for max_obj, obj_name in zip(max_objects, max_object_names): max_obj.name = f"{namespace}:{obj_name}" max_container.append(rt.getNodeByName(max_obj.name)) return containerise( name, max_container, context, namespace, loader=self.__class__.__name__) def update(self, container, representation): from pymxs import runtime as rt path = get_representation_path(representation) node_name = container["instance_node"] node = rt.getNodeByName(node_name) namespace, _ = get_namespace(node_name) # delete the old container with attribute # delete old duplicate # use the modifier OP data to delete the data node_list = get_previous_loaded_object(node) rt.select(node_list) prev_max_objects = rt.GetCurrentSelection() transform_data = object_transform_set(prev_max_objects) for prev_max_obj in prev_max_objects: if rt.isValidNode(prev_max_obj): # noqa rt.Delete(prev_max_obj) rt.MergeMaxFile(path, quiet=True) current_max_objects = rt.getLastMergedNodes() current_max_object_names = [obj.name for obj in current_max_objects] max_objects = [] for max_obj, obj_name in zip(current_max_objects, current_max_object_names): max_obj.name = f"{namespace}:{obj_name}" max_objects.append(max_obj) max_transform = f"{max_obj.name}.transform" if max_transform in transform_data.keys(): max_obj.pos = transform_data[max_transform] or 0 max_obj.scale = transform_data[ f"{max_obj.name}.scale"] or 0 update_custom_attribute_data(node, max_objects) lib.imprint(container["instance_node"], { "representation": str(representation["_id"]) }) def switch(self, container, representation): self.update(container, representation) def remove(self, container): from pymxs import runtime as rt node = rt.GetNodeByName(container["instance_node"]) rt.Delete(node) ================================================ FILE: openpype/hosts/max/plugins/load/load_model.py ================================================ import os from openpype.pipeline import load, get_representation_path from openpype.hosts.max.api.pipeline import ( containerise, get_previous_loaded_object ) from openpype.hosts.max.api import lib from openpype.hosts.max.api.lib import ( maintained_selection, unique_namespace ) class ModelAbcLoader(load.LoaderPlugin): """Loading model with the Alembic loader.""" families = ["model"] label = "Load Model with Alembic" representations = ["abc"] order = -10 icon = "code-fork" color = "orange" def load(self, context, name=None, namespace=None, data=None): from pymxs import runtime as rt file_path = os.path.normpath(self.filepath_from_context(context)) abc_before = { c for c in rt.rootNode.Children if rt.classOf(c) == rt.AlembicContainer } rt.AlembicImport.ImportToRoot = False rt.AlembicImport.CustomAttributes = True rt.AlembicImport.UVs = True rt.AlembicImport.VertexColors = True rt.importFile(file_path, rt.name("noPrompt"), using=rt.AlembicImport) abc_after = { c for c in rt.rootNode.Children if rt.classOf(c) == rt.AlembicContainer } # This should yield new AlembicContainer node abc_containers = abc_after.difference(abc_before) if len(abc_containers) != 1: self.log.error("Something failed when loading.") abc_container = abc_containers.pop() namespace = unique_namespace( name + "_", suffix="_", ) abc_objects = [] for abc_object in abc_container.Children: abc_object.name = f"{namespace}:{abc_object.name}" abc_objects.append(abc_object) # rename the abc container with namespace abc_container_name = f"{namespace}:{name}" abc_container.name = abc_container_name abc_objects.append(abc_container) return containerise( name, abc_objects, context, namespace, loader=self.__class__.__name__ ) def update(self, container, representation): from pymxs import runtime as rt path = get_representation_path(representation) node = rt.GetNodeByName(container["instance_node"]) node_list = [n for n in get_previous_loaded_object(node) if rt.ClassOf(n) == rt.AlembicContainer] with maintained_selection(): rt.Select(node_list) for alembic in rt.Selection: abc = rt.GetNodeByName(alembic.name) rt.Select(abc.Children) for abc_con in abc.Children: abc_con.source = path rt.Select(abc_con.Children) for abc_obj in abc_con.Children: abc_obj.source = path lib.imprint( container["instance_node"], {"representation": str(representation["_id"])}, ) def switch(self, container, representation): self.update(container, representation) def remove(self, container): from pymxs import runtime as rt node = rt.GetNodeByName(container["instance_node"]) rt.Delete(node) @staticmethod def get_container_children(parent, type_name): from pymxs import runtime as rt def list_children(node): children = [] for c in node.Children: children.append(c) children += list_children(c) return children filtered = [] for child in list_children(parent): class_type = str(rt.ClassOf(child.baseObject)) if class_type == type_name: filtered.append(child) return filtered ================================================ FILE: openpype/hosts/max/plugins/load/load_model_fbx.py ================================================ import os from openpype.pipeline import load, get_representation_path from openpype.hosts.max.api.pipeline import ( containerise, get_previous_loaded_object, update_custom_attribute_data ) from openpype.hosts.max.api import lib from openpype.hosts.max.api.lib import ( unique_namespace, get_namespace, object_transform_set ) from openpype.hosts.max.api.lib import maintained_selection class FbxModelLoader(load.LoaderPlugin): """Fbx Model Loader.""" families = ["model"] representations = ["fbx"] order = -9 icon = "code-fork" color = "white" def load(self, context, name=None, namespace=None, data=None): from pymxs import runtime as rt filepath = self.filepath_from_context(context) filepath = os.path.normpath(filepath) rt.FBXImporterSetParam("Animation", False) rt.FBXImporterSetParam("Cameras", False) rt.FBXImporterSetParam("Mode", rt.Name("create")) rt.FBXImporterSetParam("Preserveinstances", True) rt.importFile( filepath, rt.name("noPrompt"), using=rt.FBXIMP) namespace = unique_namespace( name + "_", suffix="_", ) selections = rt.GetCurrentSelection() for selection in selections: selection.name = f"{namespace}:{selection.name}" return containerise( name, selections, context, namespace, loader=self.__class__.__name__) def update(self, container, representation): from pymxs import runtime as rt path = get_representation_path(representation) node_name = container["instance_node"] node = rt.getNodeByName(node_name) if not node: rt.Container(name=node_name) namespace, _ = get_namespace(node_name) node_list = get_previous_loaded_object(node) rt.Select(node_list) prev_fbx_objects = rt.GetCurrentSelection() transform_data = object_transform_set(prev_fbx_objects) for prev_fbx_obj in prev_fbx_objects: if rt.isValidNode(prev_fbx_obj): rt.Delete(prev_fbx_obj) rt.FBXImporterSetParam("Animation", False) rt.FBXImporterSetParam("Cameras", False) rt.FBXImporterSetParam("Mode", rt.Name("create")) rt.FBXImporterSetParam("Preserveinstances", True) rt.importFile(path, rt.name("noPrompt"), using=rt.FBXIMP) current_fbx_objects = rt.GetCurrentSelection() fbx_objects = [] for fbx_object in current_fbx_objects: fbx_object.name = f"{namespace}:{fbx_object.name}" fbx_objects.append(fbx_object) fbx_transform = f"{fbx_object.name}.transform" if fbx_transform in transform_data.keys(): fbx_object.pos = transform_data[fbx_transform] or 0 fbx_object.scale = transform_data[ f"{fbx_object.name}.scale"] or 0 with maintained_selection(): rt.Select(node) update_custom_attribute_data(node, fbx_objects) lib.imprint(container["instance_node"], { "representation": str(representation["_id"]) }) def switch(self, container, representation): self.update(container, representation) def remove(self, container): from pymxs import runtime as rt node = rt.GetNodeByName(container["instance_node"]) rt.Delete(node) ================================================ FILE: openpype/hosts/max/plugins/load/load_model_obj.py ================================================ import os from openpype.hosts.max.api import lib from openpype.hosts.max.api.lib import ( unique_namespace, get_namespace, maintained_selection, object_transform_set ) from openpype.hosts.max.api.lib import maintained_selection from openpype.hosts.max.api.pipeline import ( containerise, get_previous_loaded_object, update_custom_attribute_data ) from openpype.pipeline import get_representation_path, load class ObjLoader(load.LoaderPlugin): """Obj Loader.""" families = ["model"] representations = ["obj"] order = -9 icon = "code-fork" color = "white" def load(self, context, name=None, namespace=None, data=None): from pymxs import runtime as rt filepath = os.path.normpath(self.filepath_from_context(context)) self.log.debug("Executing command to import..") rt.Execute(f'importFile @"{filepath}" #noPrompt using:ObjImp') namespace = unique_namespace( name + "_", suffix="_", ) # create "missing" container for obj import selections = rt.GetCurrentSelection() # get current selection for selection in selections: selection.name = f"{namespace}:{selection.name}" return containerise( name, selections, context, namespace, loader=self.__class__.__name__) def update(self, container, representation): from pymxs import runtime as rt path = get_representation_path(representation) node_name = container["instance_node"] node = rt.getNodeByName(node_name) namespace, _ = get_namespace(node_name) node_list = get_previous_loaded_object(node) rt.Select(node_list) previous_objects = rt.GetCurrentSelection() transform_data = object_transform_set(previous_objects) for prev_obj in previous_objects: if rt.isValidNode(prev_obj): rt.Delete(prev_obj) rt.Execute(f'importFile @"{path}" #noPrompt using:ObjImp') # get current selection selections = rt.GetCurrentSelection() for selection in selections: selection.name = f"{namespace}:{selection.name}" selection_transform = f"{selection.name}.transform" if selection_transform in transform_data.keys(): selection.pos = transform_data[selection_transform] or 0 selection.scale = transform_data[ f"{selection.name}.scale"] or 0 update_custom_attribute_data(node, selections) with maintained_selection(): rt.Select(node) lib.imprint(node_name, { "representation": str(representation["_id"]) }) def switch(self, container, representation): self.update(container, representation) def remove(self, container): from pymxs import runtime as rt node = rt.GetNodeByName(container["instance_node"]) rt.Delete(node) ================================================ FILE: openpype/hosts/max/plugins/load/load_model_usd.py ================================================ import os from pymxs import runtime as rt from openpype.pipeline.load import LoadError from openpype.hosts.max.api import lib from openpype.hosts.max.api.lib import ( unique_namespace, get_namespace, object_transform_set, get_plugins ) from openpype.hosts.max.api.lib import maintained_selection from openpype.hosts.max.api.pipeline import ( containerise, get_previous_loaded_object, update_custom_attribute_data ) from openpype.pipeline import get_representation_path, load class ModelUSDLoader(load.LoaderPlugin): """Loading model with the USD loader.""" families = ["model"] label = "Load Model(USD)" representations = ["usda"] order = -10 icon = "code-fork" color = "orange" def load(self, context, name=None, namespace=None, data=None): # asset_filepath plugin_info = get_plugins() if "usdimport.dli" not in plugin_info: raise LoadError("No USDImporter loaded/installed in Max..") filepath = os.path.normpath(self.filepath_from_context(context)) import_options = rt.USDImporter.CreateOptions() base_filename = os.path.basename(filepath) _, ext = os.path.splitext(base_filename) log_filepath = filepath.replace(ext, "txt") rt.LogPath = log_filepath rt.LogLevel = rt.Name("info") rt.USDImporter.importFile(filepath, importOptions=import_options) namespace = unique_namespace( name + "_", suffix="_", ) asset = rt.GetNodeByName(name) usd_objects = [] for usd_asset in asset.Children: usd_asset.name = f"{namespace}:{usd_asset.name}" usd_objects.append(usd_asset) asset_name = f"{namespace}:{name}" asset.name = asset_name # need to get the correct container after renamed asset = rt.GetNodeByName(asset_name) usd_objects.append(asset) return containerise( name, usd_objects, context, namespace, loader=self.__class__.__name__) def update(self, container, representation): path = get_representation_path(representation) node_name = container["instance_node"] node = rt.GetNodeByName(node_name) namespace, name = get_namespace(node_name) node_list = get_previous_loaded_object(node) rt.Select(node_list) prev_objects = [sel for sel in rt.GetCurrentSelection() if sel != rt.Container and sel.name != node_name] transform_data = object_transform_set(prev_objects) for n in prev_objects: rt.Delete(n) import_options = rt.USDImporter.CreateOptions() base_filename = os.path.basename(path) _, ext = os.path.splitext(base_filename) log_filepath = path.replace(ext, "txt") rt.LogPath = log_filepath rt.LogLevel = rt.Name("info") rt.USDImporter.importFile( path, importOptions=import_options) asset = rt.GetNodeByName(name) usd_objects = [] for children in asset.Children: children.name = f"{namespace}:{children.name}" usd_objects.append(children) children_transform = f"{children.name}.transform" if children_transform in transform_data.keys(): children.pos = transform_data[children_transform] or 0 children.scale = transform_data[ f"{children.name}.scale"] or 0 asset.name = f"{namespace}:{asset.name}" usd_objects.append(asset) update_custom_attribute_data(node, usd_objects) with maintained_selection(): rt.Select(node) lib.imprint(node_name, { "representation": str(representation["_id"]) }) def switch(self, container, representation): self.update(container, representation) def remove(self, container): node = rt.GetNodeByName(container["instance_node"]) rt.Delete(node) ================================================ FILE: openpype/hosts/max/plugins/load/load_pointcache.py ================================================ # -*- coding: utf-8 -*- """Simple alembic loader for 3dsmax. Because of limited api, alembics can be only loaded, but not easily updated. """ import os from openpype.pipeline import load, get_representation_path from openpype.hosts.max.api import lib, maintained_selection from openpype.hosts.max.api.lib import unique_namespace from openpype.hosts.max.api.pipeline import ( containerise, get_previous_loaded_object ) class AbcLoader(load.LoaderPlugin): """Alembic loader.""" families = ["camera", "animation", "pointcache"] label = "Load Alembic" representations = ["abc"] order = -10 icon = "code-fork" color = "orange" def load(self, context, name=None, namespace=None, data=None): from pymxs import runtime as rt file_path = self.filepath_from_context(context) file_path = os.path.normpath(file_path) abc_before = { c for c in rt.rootNode.Children if rt.classOf(c) == rt.AlembicContainer } rt.AlembicImport.ImportToRoot = False rt.importFile(file_path, rt.name("noPrompt"), using=rt.AlembicImport) abc_after = { c for c in rt.rootNode.Children if rt.classOf(c) == rt.AlembicContainer } # This should yield new AlembicContainer node abc_containers = abc_after.difference(abc_before) if len(abc_containers) != 1: self.log.error("Something failed when loading.") abc_container = abc_containers.pop() selections = rt.GetCurrentSelection() for abc in selections: for cam_shape in abc.Children: cam_shape.playbackType = 0 namespace = unique_namespace( name + "_", suffix="_", ) abc_objects = [] for abc_object in abc_container.Children: abc_object.name = f"{namespace}:{abc_object.name}" abc_objects.append(abc_object) # rename the abc container with namespace abc_container_name = f"{namespace}:{name}" abc_container.name = abc_container_name abc_objects.append(abc_container) return containerise( name, abc_objects, context, namespace, loader=self.__class__.__name__ ) def update(self, container, representation): from pymxs import runtime as rt path = get_representation_path(representation) node = rt.GetNodeByName(container["instance_node"]) abc_container = [n for n in get_previous_loaded_object(node) if rt.ClassOf(n) == rt.AlembicContainer] with maintained_selection(): rt.Select(abc_container) for alembic in rt.Selection: abc = rt.GetNodeByName(alembic.name) rt.Select(abc.Children) for abc_con in abc.Children: abc_con.source = path rt.Select(abc_con.Children) for abc_obj in abc_con.Children: abc_obj.source = path lib.imprint( container["instance_node"], {"representation": str(representation["_id"])}, ) def switch(self, container, representation): self.update(container, representation) def remove(self, container): from pymxs import runtime as rt node = rt.GetNodeByName(container["instance_node"]) rt.Delete(node) @staticmethod def get_container_children(parent, type_name): from pymxs import runtime as rt def list_children(node): children = [] for c in node.Children: children.append(c) children += list_children(c) return children filtered = [] for child in list_children(parent): class_type = str(rt.classOf(child.baseObject)) if class_type == type_name: filtered.append(child) return filtered ================================================ FILE: openpype/hosts/max/plugins/load/load_pointcache_ornatrix.py ================================================ import os from openpype.pipeline import load, get_representation_path from openpype.pipeline.load import LoadError from openpype.hosts.max.api.pipeline import ( containerise, get_previous_loaded_object, update_custom_attribute_data ) from openpype.hosts.max.api.lib import ( unique_namespace, get_namespace, object_transform_set, get_plugins ) from openpype.hosts.max.api import lib from pymxs import runtime as rt class OxAbcLoader(load.LoaderPlugin): """Ornatrix Alembic loader.""" families = ["camera", "animation", "pointcache"] label = "Load Alembic with Ornatrix" representations = ["abc"] order = -10 icon = "code-fork" color = "orange" postfix = "param" def load(self, context, name=None, namespace=None, data=None): plugin_list = get_plugins() if "ephere.plugins.autodesk.max.ornatrix.dlo" not in plugin_list: raise LoadError("Ornatrix plugin not " "found/installed in Max yet..") file_path = os.path.normpath(self.filepath_from_context(context)) rt.AlembicImport.ImportToRoot = True rt.AlembicImport.CustomAttributes = True rt.importFile( file_path, rt.name("noPrompt"), using=rt.Ornatrix_Alembic_Importer) scene_object = [] for obj in rt.rootNode.Children: obj_type = rt.ClassOf(obj) if str(obj_type).startswith("Ox_"): scene_object.append(obj) namespace = unique_namespace( name + "_", suffix="_", ) abc_container = [] for abc in scene_object: abc.name = f"{namespace}:{abc.name}" abc_container.append(abc) return containerise( name, abc_container, context, namespace, loader=self.__class__.__name__ ) def update(self, container, representation): path = get_representation_path(representation) node_name = container["instance_node"] namespace, name = get_namespace(node_name) node = rt.getNodeByName(node_name) node_list = get_previous_loaded_object(node) rt.Select(node_list) selections = rt.getCurrentSelection() transform_data = object_transform_set(selections) for prev_obj in selections: if rt.isValidNode(prev_obj): rt.Delete(prev_obj) rt.AlembicImport.ImportToRoot = False rt.AlembicImport.CustomAttributes = True rt.importFile( path, rt.name("noPrompt"), using=rt.Ornatrix_Alembic_Importer) scene_object = [] for obj in rt.rootNode.Children: obj_type = rt.ClassOf(obj) if str(obj_type).startswith("Ox_"): scene_object.append(obj) ox_abc_objects = [] for abc in scene_object: abc.Parent = container abc.name = f"{namespace}:{abc.name}" ox_abc_objects.append(abc) ox_transform = f"{abc.name}.transform" if ox_transform in transform_data.keys(): abc.pos = transform_data[ox_transform] or 0 abc.scale = transform_data[f"{abc.name}.scale"] or 0 update_custom_attribute_data(node, ox_abc_objects) lib.imprint( container["instance_node"], {"representation": str(representation["_id"])}, ) def switch(self, container, representation): self.update(container, representation) def remove(self, container): node = rt.GetNodeByName(container["instance_node"]) rt.Delete(node) ================================================ FILE: openpype/hosts/max/plugins/load/load_pointcloud.py ================================================ import os from openpype.hosts.max.api import lib, maintained_selection from openpype.hosts.max.api.lib import ( unique_namespace, ) from openpype.hosts.max.api.pipeline import ( containerise, get_previous_loaded_object, update_custom_attribute_data ) from openpype.pipeline import get_representation_path, load class PointCloudLoader(load.LoaderPlugin): """Point Cloud Loader.""" families = ["pointcloud"] representations = ["prt"] order = -8 icon = "code-fork" color = "green" postfix = "param" def load(self, context, name=None, namespace=None, data=None): """load point cloud by tyCache""" from pymxs import runtime as rt filepath = os.path.normpath(self.filepath_from_context(context)) obj = rt.tyCache() obj.filename = filepath namespace = unique_namespace( name + "_", suffix="_", ) obj.name = f"{namespace}:{obj.name}" return containerise( name, [obj], context, namespace, loader=self.__class__.__name__) def update(self, container, representation): """update the container""" from pymxs import runtime as rt path = get_representation_path(representation) node = rt.GetNodeByName(container["instance_node"]) node_list = get_previous_loaded_object(node) update_custom_attribute_data( node, node_list) with maintained_selection(): rt.Select(node_list) for prt in rt.Selection: prt.filename = path lib.imprint(container["instance_node"], { "representation": str(representation["_id"]) }) def switch(self, container, representation): self.update(container, representation) def remove(self, container): """remove the container""" from pymxs import runtime as rt node = rt.GetNodeByName(container["instance_node"]) rt.Delete(node) ================================================ FILE: openpype/hosts/max/plugins/load/load_redshift_proxy.py ================================================ import os import clique from openpype.pipeline import ( load, get_representation_path ) from openpype.pipeline.load import LoadError from openpype.hosts.max.api.pipeline import ( containerise, update_custom_attribute_data, get_previous_loaded_object ) from openpype.hosts.max.api import lib from openpype.hosts.max.api.lib import ( unique_namespace, get_plugins ) class RedshiftProxyLoader(load.LoaderPlugin): """Load rs files with Redshift Proxy""" label = "Load Redshift Proxy" families = ["redshiftproxy"] representations = ["rs"] order = -9 icon = "code-fork" color = "white" def load(self, context, name=None, namespace=None, data=None): from pymxs import runtime as rt plugin_info = get_plugins() if "redshift4max.dlr" not in plugin_info: raise LoadError("Redshift not loaded/installed in Max..") filepath = self.filepath_from_context(context) rs_proxy = rt.RedshiftProxy() rs_proxy.file = filepath files_in_folder = os.listdir(os.path.dirname(filepath)) collections, remainder = clique.assemble(files_in_folder) if collections: rs_proxy.is_sequence = True namespace = unique_namespace( name + "_", suffix="_", ) rs_proxy.name = f"{namespace}:{rs_proxy.name}" return containerise( name, [rs_proxy], context, namespace, loader=self.__class__.__name__) def update(self, container, representation): from pymxs import runtime as rt path = get_representation_path(representation) node = rt.getNodeByName(container["instance_node"]) node_list = get_previous_loaded_object(node) rt.Select(node_list) update_custom_attribute_data( node, rt.Selection) for proxy in rt.Selection: proxy.file = path lib.imprint(container["instance_node"], { "representation": str(representation["_id"]) }) def switch(self, container, representation): self.update(container, representation) def remove(self, container): from pymxs import runtime as rt node = rt.getNodeByName(container["instance_node"]) rt.delete(node) ================================================ FILE: openpype/hosts/max/plugins/load/load_tycache.py ================================================ import os from openpype.hosts.max.api import lib, maintained_selection from openpype.hosts.max.api.lib import ( unique_namespace, ) from openpype.hosts.max.api.pipeline import ( containerise, get_previous_loaded_object, update_custom_attribute_data ) from openpype.pipeline import get_representation_path, load class TyCacheLoader(load.LoaderPlugin): """TyCache Loader.""" families = ["tycache"] representations = ["tyc"] order = -8 icon = "code-fork" color = "green" def load(self, context, name=None, namespace=None, data=None): """Load tyCache""" from pymxs import runtime as rt filepath = os.path.normpath(self.filepath_from_context(context)) obj = rt.tyCache() obj.filename = filepath namespace = unique_namespace( name + "_", suffix="_", ) obj.name = f"{namespace}:{obj.name}" return containerise( name, [obj], context, namespace, loader=self.__class__.__name__) def update(self, container, representation): """update the container""" from pymxs import runtime as rt path = get_representation_path(representation) node = rt.GetNodeByName(container["instance_node"]) node_list = get_previous_loaded_object(node) update_custom_attribute_data(node, node_list) with maintained_selection(): for tyc in node_list: tyc.filename = path lib.imprint(container["instance_node"], { "representation": str(representation["_id"]) }) def switch(self, container, representation): self.update(container, representation) def remove(self, container): """remove the container""" from pymxs import runtime as rt node = rt.GetNodeByName(container["instance_node"]) rt.Delete(node) ================================================ FILE: openpype/hosts/max/plugins/publish/collect_frame_range.py ================================================ # -*- coding: utf-8 -*- import pyblish.api from pymxs import runtime as rt class CollectFrameRange(pyblish.api.InstancePlugin): """Collect Frame Range.""" order = pyblish.api.CollectorOrder + 0.01 label = "Collect Frame Range" hosts = ['max'] families = ["camera", "maxrender", "pointcache", "pointcloud", "review", "redshiftproxy"] def process(self, instance): if instance.data["family"] == "maxrender": instance.data["frameStartHandle"] = int(rt.rendStart) instance.data["frameEndHandle"] = int(rt.rendEnd) else: instance.data["frameStartHandle"] = int(rt.animationRange.start) instance.data["frameEndHandle"] = int(rt.animationRange.end) ================================================ FILE: openpype/hosts/max/plugins/publish/collect_members.py ================================================ # -*- coding: utf-8 -*- """Collect instance members.""" import pyblish.api from pymxs import runtime as rt class CollectMembers(pyblish.api.InstancePlugin): """Collect Set Members.""" order = pyblish.api.CollectorOrder + 0.01 label = "Collect Instance Members" hosts = ['max'] def process(self, instance): if instance.data["family"] == "workfile": self.log.debug("Skipping Actions for workfile family.") self.log.debug("{}".format(instance.data["subset"])) return if instance.data.get("instance_node"): container = rt.GetNodeByName(instance.data["instance_node"]) instance.data["members"] = [ member.node for member in container.modifiers[0].openPypeData.all_handles ] self.log.debug("{}".format(instance.data["members"])) ================================================ FILE: openpype/hosts/max/plugins/publish/collect_render.py ================================================ # -*- coding: utf-8 -*- """Collect Render""" import os import pyblish.api from pymxs import runtime as rt from openpype.pipeline.publish import KnownPublishError from openpype.hosts.max.api import colorspace from openpype.hosts.max.api.lib import get_max_version, get_current_renderer from openpype.hosts.max.api.lib_rendersettings import RenderSettings from openpype.hosts.max.api.lib_renderproducts import RenderProducts class CollectRender(pyblish.api.InstancePlugin): """Collect Render for Deadline""" order = pyblish.api.CollectorOrder + 0.02 label = "Collect 3dsmax Render Layers" hosts = ['max'] families = ["maxrender"] def process(self, instance): context = instance.context folder = rt.maxFilePath file = rt.maxFileName current_file = os.path.join(folder, file) filepath = current_file.replace("\\", "/") context.data['currentFile'] = current_file files_by_aov = RenderProducts().get_beauty(instance.name) aovs = RenderProducts().get_aovs(instance.name) files_by_aov.update(aovs) camera = rt.viewport.GetCamera() if instance.data.get("members"): camera_list = [member for member in instance.data["members"] if rt.ClassOf(member) == rt.Camera.Classes] if camera_list: camera = camera_list[-1] instance.data["cameras"] = [camera.name] if camera else None # noqa if instance.data.get("multiCamera"): cameras = instance.data.get("members") if not cameras: raise KnownPublishError("There should be at least" " one renderable camera in container") sel_cam = [ c.name for c in cameras if rt.classOf(c) in rt.Camera.classes] container_name = instance.data.get("instance_node") render_dir = os.path.dirname(rt.rendOutputFilename) outputs = RenderSettings().batch_render_layer( container_name, render_dir, sel_cam ) instance.data["cameras"] = sel_cam files_by_aov = RenderProducts().get_multiple_beauty( outputs, sel_cam) aovs = RenderProducts().get_multiple_aovs( outputs, sel_cam) files_by_aov.update(aovs) if "expectedFiles" not in instance.data: instance.data["expectedFiles"] = list() instance.data["files"] = list() instance.data["expectedFiles"].append(files_by_aov) instance.data["files"].append(files_by_aov) img_format = RenderProducts().image_format() # OCIO config not support in # most of the 3dsmax renderers # so this is currently hard coded # TODO: add options for redshift/vray ocio config instance.data["colorspaceConfig"] = "" instance.data["colorspaceDisplay"] = "sRGB" instance.data["colorspaceView"] = "ACES 1.0 SDR-video" if int(get_max_version()) >= 2024: colorspace_mgr = rt.ColorPipelineMgr # noqa display = next( (display for display in colorspace_mgr.GetDisplayList())) view_transform = next( (view for view in colorspace_mgr.GetViewList(display))) instance.data["colorspaceConfig"] = colorspace_mgr.OCIOConfigPath instance.data["colorspaceDisplay"] = display instance.data["colorspaceView"] = view_transform instance.data["renderProducts"] = colorspace.ARenderProduct() instance.data["publishJobState"] = "Suspended" instance.data["attachTo"] = [] renderer_class = get_current_renderer() renderer = str(renderer_class).split(":")[0] # also need to get the render dir for conversion data = { "asset": instance.data["asset"], "subset": str(instance.name), "publish": True, "maxversion": str(get_max_version()), "imageFormat": img_format, "family": 'maxrender', "families": ['maxrender'], "renderer": renderer, "source": filepath, "plugin": "3dsmax", "frameStart": instance.data["frameStartHandle"], "frameEnd": instance.data["frameEndHandle"], "farm": True } instance.data.update(data) # TODO: this should be unified with maya and its "multipart" flag # on instance. if renderer == "Redshift_Renderer": instance.data.update( {"separateAovFiles": rt.Execute( "renderers.current.separateAovFiles")}) self.log.info("data: {0}".format(data)) ================================================ FILE: openpype/hosts/max/plugins/publish/collect_review.py ================================================ # dont forget getting the focal length for burnin """Collect Review""" import pyblish.api from pymxs import runtime as rt from openpype.lib import BoolDef from openpype.hosts.max.api.lib import get_max_version from openpype.pipeline.publish import ( OpenPypePyblishPluginMixin, KnownPublishError ) class CollectReview(pyblish.api.InstancePlugin, OpenPypePyblishPluginMixin): """Collect Review Data for Preview Animation""" order = pyblish.api.CollectorOrder + 0.02 label = "Collect Review Data" hosts = ['max'] families = ["review"] def process(self, instance): nodes = instance.data["members"] def is_camera(node): is_camera_class = rt.classOf(node) in rt.Camera.classes return is_camera_class and rt.isProperty(node, "fov") # Use first camera in instance cameras = [node for node in nodes if is_camera(node)] if cameras: if len(cameras) > 1: self.log.warning( "Found more than one camera in instance, using first " f"one found: {cameras[0]}" ) camera = cameras[0] camera_name = camera.name focal_length = camera.fov else: raise KnownPublishError( "Unable to find a valid camera in 'Review' container." " Only native max Camera supported. " f"Found objects: {nodes}" ) creator_attrs = instance.data["creator_attributes"] attr_values = self.get_attr_values_from_data(instance.data) general_preview_data = { "review_camera": camera_name, "frameStart": instance.data["frameStartHandle"], "frameEnd": instance.data["frameEndHandle"], "percentSize": creator_attrs["percentSize"], "imageFormat": creator_attrs["imageFormat"], "keepImages": creator_attrs["keepImages"], "fps": instance.context.data["fps"], "review_width": creator_attrs["review_width"], "review_height": creator_attrs["review_height"], } if int(get_max_version()) >= 2024: colorspace_mgr = rt.ColorPipelineMgr # noqa display = next( (display for display in colorspace_mgr.GetDisplayList())) view_transform = next( (view for view in colorspace_mgr.GetViewList(display))) instance.data["colorspaceConfig"] = colorspace_mgr.OCIOConfigPath instance.data["colorspaceDisplay"] = display instance.data["colorspaceView"] = view_transform preview_data = { "vpStyle": creator_attrs["visualStyleMode"], "vpPreset": creator_attrs["viewportPreset"], "vpTextures": creator_attrs["vpTexture"], "dspGeometry": attr_values.get("dspGeometry"), "dspShapes": attr_values.get("dspShapes"), "dspLights": attr_values.get("dspLights"), "dspCameras": attr_values.get("dspCameras"), "dspHelpers": attr_values.get("dspHelpers"), "dspParticles": attr_values.get("dspParticles"), "dspBones": attr_values.get("dspBones"), "dspBkg": attr_values.get("dspBkg"), "dspGrid": attr_values.get("dspGrid"), "dspSafeFrame": attr_values.get("dspSafeFrame"), "dspFrameNums": attr_values.get("dspFrameNums") } else: general_viewport = { "dspBkg": attr_values.get("dspBkg"), "dspGrid": attr_values.get("dspGrid") } nitrous_manager = { "AntialiasingQuality": creator_attrs["antialiasingQuality"], } nitrous_viewport = { "VisualStyleMode": creator_attrs["visualStyleMode"], "ViewportPreset": creator_attrs["viewportPreset"], "UseTextureEnabled": creator_attrs["vpTexture"] } preview_data = { "general_viewport": general_viewport, "nitrous_manager": nitrous_manager, "nitrous_viewport": nitrous_viewport, "vp_btn_mgr": {"EnableButtons": False} } # Enable ftrack functionality instance.data.setdefault("families", []).append('ftrack') burnin_members = instance.data.setdefault("burninDataMembers", {}) burnin_members["focalLength"] = focal_length instance.data.update(general_preview_data) instance.data["viewport_options"] = preview_data @classmethod def get_attribute_defs(cls): return [ BoolDef("dspGeometry", label="Geometry", default=True), BoolDef("dspShapes", label="Shapes", default=False), BoolDef("dspLights", label="Lights", default=False), BoolDef("dspCameras", label="Cameras", default=False), BoolDef("dspHelpers", label="Helpers", default=False), BoolDef("dspParticles", label="Particle Systems", default=True), BoolDef("dspBones", label="Bone Objects", default=False), BoolDef("dspBkg", label="Background", default=True), BoolDef("dspGrid", label="Active Grid", default=False), BoolDef("dspSafeFrame", label="Safe Frames", default=False), BoolDef("dspFrameNums", label="Frame Numbers", default=False) ] ================================================ FILE: openpype/hosts/max/plugins/publish/collect_tycache_attributes.py ================================================ import pyblish.api from openpype.lib import EnumDef, TextDef from openpype.pipeline.publish import OpenPypePyblishPluginMixin class CollectTyCacheData(pyblish.api.InstancePlugin, OpenPypePyblishPluginMixin): """Collect Channel Attributes for TyCache Export""" order = pyblish.api.CollectorOrder + 0.02 label = "Collect tyCache attribute Data" hosts = ['max'] families = ["tycache"] def process(self, instance): attr_values = self.get_attr_values_from_data(instance.data) attributes = {} for attr_key in attr_values.get("tycacheAttributes", []): attributes[attr_key] = True for key in ["tycacheLayer", "tycacheObjectName"]: attributes[key] = attr_values.get(key, "") # Collect the selected channel data before exporting instance.data["tyc_attrs"] = attributes self.log.debug( f"Found tycache attributes: {attributes}" ) @classmethod def get_attribute_defs(cls): # TODO: Support the attributes with maxObject array tyc_attr_enum = ["tycacheChanAge", "tycacheChanGroups", "tycacheChanPos", "tycacheChanRot", "tycacheChanScale", "tycacheChanVel", "tycacheChanSpin", "tycacheChanShape", "tycacheChanMatID", "tycacheChanMapping", "tycacheChanMaterials", "tycacheChanCustomFloat" "tycacheChanCustomVector", "tycacheChanCustomTM", "tycacheChanPhysX", "tycacheMeshBackup", "tycacheCreateObject", "tycacheCreateObjectIfNotCreated", "tycacheAdditionalCloth", "tycacheAdditionalSkin", "tycacheAdditionalSkinID", "tycacheAdditionalSkinIDValue", "tycacheAdditionalTerrain", "tycacheAdditionalVDB", "tycacheAdditionalSplinePaths", "tycacheAdditionalGeo", "tycacheAdditionalGeoActivateModifiers", "tycacheSplines", "tycacheSplinesAdditionalSplines" ] tyc_default_attrs = ["tycacheChanGroups", "tycacheChanPos", "tycacheChanRot", "tycacheChanScale", "tycacheChanVel", "tycacheChanShape", "tycacheChanMatID", "tycacheChanMapping", "tycacheChanMaterials", "tycacheCreateObjectIfNotCreated"] return [ EnumDef("tycacheAttributes", tyc_attr_enum, default=tyc_default_attrs, multiselection=True, label="TyCache Attributes"), TextDef("tycacheLayer", label="TyCache Layer", tooltip="Name of tycache layer", default="$(tyFlowLayer)"), TextDef("tycacheObjectName", label="TyCache Object Name", tooltip="TyCache Object Name", default="$(tyFlowName)_tyCache") ] ================================================ FILE: openpype/hosts/max/plugins/publish/collect_workfile.py ================================================ # -*- coding: utf-8 -*- """Collect current work file.""" import os import pyblish.api from pymxs import runtime as rt class CollectWorkfile(pyblish.api.InstancePlugin): """Inject the current working file into context""" order = pyblish.api.CollectorOrder - 0.01 label = "Collect 3dsmax Workfile" hosts = ['max'] def process(self, instance): """Inject the current working file.""" context = instance.context folder = rt.maxFilePath file = rt.maxFileName if not folder or not file: self.log.error("Scene is not saved.") current_file = os.path.join(folder, file) context.data['currentFile'] = current_file ext = os.path.splitext(file)[-1].lstrip(".") data = {} # create instance subset = instance.data["subset"] data.update({ "subset": subset, "asset": context.data["asset"], "label": subset, "publish": True, "family": 'workfile', "families": ['workfile'], "setMembers": [current_file], "frameStart": context.data['frameStart'], "frameEnd": context.data['frameEnd'], "handleStart": context.data['handleStart'], "handleEnd": context.data['handleEnd'] }) data['representations'] = [{ 'name': ext.lstrip("."), 'ext': ext.lstrip("."), 'files': file, "stagingDir": folder, }] instance.data.update(data) self.log.info('Collected data: {}'.format(data)) self.log.info('Collected instance: {}'.format(file)) self.log.info('Scene path: {}'.format(current_file)) self.log.info('staging Dir: {}'.format(folder)) self.log.info('subset: {}'.format(subset)) ================================================ FILE: openpype/hosts/max/plugins/publish/extract_alembic.py ================================================ # -*- coding: utf-8 -*- """ Export alembic file. Note: Parameters on AlembicExport (AlembicExport.Parameter): ParticleAsMesh (bool): Sets whether particle shapes are exported as meshes. AnimTimeRange (enum): How animation is saved: #CurrentFrame: saves current frame #TimeSlider: saves the active time segments on time slider (default) #StartEnd: saves a range specified by the Step StartFrame (int) EnFrame (int) ShapeSuffix (bool): When set to true, appends the string "Shape" to the name of each exported mesh. This property is set to false by default. SamplesPerFrame (int): Sets the number of animation samples per frame. Hidden (bool): When true, export hidden geometry. UVs (bool): When true, export the mesh UV map channel. Normals (bool): When true, export the mesh normals. VertexColors (bool): When true, export the mesh vertex color map 0 and the current vertex color display data when it differs ExtraChannels (bool): When true, export the mesh extra map channels (map channels greater than channel 1) Velocity (bool): When true, export the meh vertex and particle velocity data. MaterialIDs (bool): When true, export the mesh material ID as Alembic face sets. Visibility (bool): When true, export the node visibility data. LayerName (bool): When true, export the node layer name as an Alembic object property. MaterialName (bool): When true, export the geometry node material name as an Alembic object property ObjectID (bool): When true, export the geometry node g-buffer object ID as an Alembic object property. CustomAttributes (bool): When true, export the node and its modifiers custom attributes into an Alembic object compound property. """ import os import pyblish.api from openpype.pipeline import publish, OptionalPyblishPluginMixin from pymxs import runtime as rt from openpype.hosts.max.api import maintained_selection from openpype.hosts.max.api.lib import suspended_refresh from openpype.lib import BoolDef class ExtractAlembic(publish.Extractor, OptionalPyblishPluginMixin): order = pyblish.api.ExtractorOrder label = "Extract Pointcache" hosts = ["max"] families = ["pointcache"] optional = True def process(self, instance): if not self.is_active(instance.data): return parent_dir = self.staging_dir(instance) file_name = "{name}.abc".format(**instance.data) path = os.path.join(parent_dir, file_name) with suspended_refresh(): self._set_abc_attributes(instance) with maintained_selection(): # select and export node_list = instance.data["members"] rt.Select(node_list) rt.exportFile( path, rt.name("noPrompt"), selectedOnly=True, using=rt.AlembicExport, ) if "representations" not in instance.data: instance.data["representations"] = [] representation = { "name": "abc", "ext": "abc", "files": file_name, "stagingDir": parent_dir, } instance.data["representations"].append(representation) def _set_abc_attributes(self, instance): start = instance.data["frameStartHandle"] end = instance.data["frameEndHandle"] attr_values = self.get_attr_values_from_data(instance.data) custom_attrs = attr_values.get("custom_attrs", False) if not custom_attrs: self.log.debug( "No Custom Attributes included in this abc export...") rt.AlembicExport.ArchiveType = rt.Name("ogawa") rt.AlembicExport.CoordinateSystem = rt.Name("maya") rt.AlembicExport.StartFrame = start rt.AlembicExport.EndFrame = end rt.AlembicExport.CustomAttributes = custom_attrs @classmethod def get_attribute_defs(cls): return [ BoolDef("custom_attrs", label="Custom Attributes", default=False), ] class ExtractCameraAlembic(ExtractAlembic): """Extract Camera with AlembicExport.""" label = "Extract Alembic Camera" families = ["camera"] class ExtractModel(ExtractAlembic): """Extract Geometry in Alembic Format""" label = "Extract Geometry (Alembic)" families = ["model"] def _set_abc_attributes(self, instance): attr_values = self.get_attr_values_from_data(instance.data) custom_attrs = attr_values.get("custom_attrs", False) if not custom_attrs: self.log.debug( "No Custom Attributes included in this abc export...") rt.AlembicExport.ArchiveType = rt.name("ogawa") rt.AlembicExport.CoordinateSystem = rt.name("maya") rt.AlembicExport.CustomAttributes = custom_attrs rt.AlembicExport.UVs = True rt.AlembicExport.VertexColors = True rt.AlembicExport.PreserveInstances = True ================================================ FILE: openpype/hosts/max/plugins/publish/extract_fbx.py ================================================ import os import pyblish.api from openpype.pipeline import publish, OptionalPyblishPluginMixin from pymxs import runtime as rt from openpype.hosts.max.api import maintained_selection from openpype.hosts.max.api.lib import convert_unit_scale class ExtractModelFbx(publish.Extractor, OptionalPyblishPluginMixin): """ Extract Geometry in FBX Format """ order = pyblish.api.ExtractorOrder - 0.05 label = "Extract FBX" hosts = ["max"] families = ["model"] optional = True def process(self, instance): if not self.is_active(instance.data): return stagingdir = self.staging_dir(instance) filename = "{name}.fbx".format(**instance.data) filepath = os.path.join(stagingdir, filename) self._set_fbx_attributes() with maintained_selection(): # select and export node_list = instance.data["members"] rt.Select(node_list) rt.exportFile( filepath, rt.name("noPrompt"), selectedOnly=True, using=rt.FBXEXP, ) if "representations" not in instance.data: instance.data["representations"] = [] representation = { "name": "fbx", "ext": "fbx", "files": filename, "stagingDir": stagingdir, } instance.data["representations"].append(representation) self.log.info( "Extracted instance '%s' to: %s" % (instance.name, filepath) ) def _set_fbx_attributes(self): unit_scale = convert_unit_scale() rt.FBXExporterSetParam("Animation", False) rt.FBXExporterSetParam("Cameras", False) rt.FBXExporterSetParam("Lights", False) rt.FBXExporterSetParam("PointCache", False) rt.FBXExporterSetParam("AxisConversionMethod", "Animation") rt.FBXExporterSetParam("UpAxis", "Y") rt.FBXExporterSetParam("Preserveinstances", True) if unit_scale: rt.FBXExporterSetParam("ConvertUnit", unit_scale) class ExtractCameraFbx(ExtractModelFbx): """Extract Camera with FbxExporter.""" order = pyblish.api.ExtractorOrder - 0.2 label = "Extract Fbx Camera" families = ["camera"] optional = True def _set_fbx_attributes(self): unit_scale = convert_unit_scale() rt.FBXExporterSetParam("Animation", True) rt.FBXExporterSetParam("Cameras", True) rt.FBXExporterSetParam("AxisConversionMethod", "Animation") rt.FBXExporterSetParam("UpAxis", "Y") rt.FBXExporterSetParam("Preserveinstances", True) if unit_scale: rt.FBXExporterSetParam("ConvertUnit", unit_scale) ================================================ FILE: openpype/hosts/max/plugins/publish/extract_max_scene_raw.py ================================================ import os import pyblish.api from openpype.pipeline import publish, OptionalPyblishPluginMixin from pymxs import runtime as rt class ExtractMaxSceneRaw(publish.Extractor, OptionalPyblishPluginMixin): """ Extract Raw Max Scene with SaveSelected """ order = pyblish.api.ExtractorOrder - 0.2 label = "Extract Max Scene (Raw)" hosts = ["max"] families = ["camera", "maxScene", "model"] optional = True def process(self, instance): if not self.is_active(instance.data): return # publish the raw scene for camera self.log.debug("Extracting Raw Max Scene ...") stagingdir = self.staging_dir(instance) filename = "{name}.max".format(**instance.data) max_path = os.path.join(stagingdir, filename) if "representations" not in instance.data: instance.data["representations"] = [] nodes = instance.data["members"] rt.saveNodes(nodes, max_path, quiet=True) self.log.info("Performing Extraction ...") representation = { "name": "max", "ext": "max", "files": filename, "stagingDir": stagingdir, } instance.data["representations"].append(representation) self.log.info( "Extracted instance '%s' to: %s" % (instance.name, max_path) ) ================================================ FILE: openpype/hosts/max/plugins/publish/extract_model_obj.py ================================================ import os import pyblish.api from openpype.pipeline import publish, OptionalPyblishPluginMixin from pymxs import runtime as rt from openpype.hosts.max.api import maintained_selection from openpype.hosts.max.api.lib import suspended_refresh from openpype.pipeline.publish import KnownPublishError class ExtractModelObj(publish.Extractor, OptionalPyblishPluginMixin): """ Extract Geometry in OBJ Format """ order = pyblish.api.ExtractorOrder - 0.05 label = "Extract OBJ" hosts = ["max"] families = ["model"] optional = True def process(self, instance): if not self.is_active(instance.data): return stagingdir = self.staging_dir(instance) filename = "{name}.obj".format(**instance.data) filepath = os.path.join(stagingdir, filename) with suspended_refresh(): with maintained_selection(): # select and export node_list = instance.data["members"] rt.Select(node_list) rt.exportFile( filepath, rt.name("noPrompt"), selectedOnly=True, using=rt.ObjExp, ) if not os.path.exists(filepath): raise KnownPublishError( "File {} wasn't produced by 3ds max, please check the logs.") if "representations" not in instance.data: instance.data["representations"] = [] representation = { "name": "obj", "ext": "obj", "files": filename, "stagingDir": stagingdir, } instance.data["representations"].append(representation) self.log.info( "Extracted instance '%s' to: %s" % (instance.name, filepath) ) ================================================ FILE: openpype/hosts/max/plugins/publish/extract_model_usd.py ================================================ import os import pyblish.api from pymxs import runtime as rt from openpype.hosts.max.api import maintained_selection from openpype.pipeline import OptionalPyblishPluginMixin, publish class ExtractModelUSD(publish.Extractor, OptionalPyblishPluginMixin): """Extract Geometry in USDA Format.""" order = pyblish.api.ExtractorOrder - 0.05 label = "Extract Geometry (USD)" hosts = ["max"] families = ["model"] optional = True def process(self, instance): if not self.is_active(instance.data): return self.log.info("Extracting Geometry ...") stagingdir = self.staging_dir(instance) asset_filename = "{name}.usda".format(**instance.data) asset_filepath = os.path.join(stagingdir, asset_filename) self.log.info(f"Writing USD '{asset_filepath}' to '{stagingdir}'") log_filename = "{name}.txt".format(**instance.data) log_filepath = os.path.join(stagingdir, log_filename) self.log.info(f"Writing log '{log_filepath}' to '{stagingdir}'") # get the nodes which need to be exported export_options = self.get_export_options(log_filepath) with maintained_selection(): # select and export node_list = instance.data["members"] rt.Select(node_list) rt.USDExporter.ExportFile(asset_filepath, exportOptions=export_options, contentSource=rt.Name("selected"), nodeList=node_list) self.log.info("Performing Extraction ...") if "representations" not in instance.data: instance.data["representations"] = [] representation = { 'name': 'usda', 'ext': 'usda', 'files': asset_filename, "stagingDir": stagingdir, } instance.data["representations"].append(representation) log_representation = { 'name': 'txt', 'ext': 'txt', 'files': log_filename, "stagingDir": stagingdir, } instance.data["representations"].append(log_representation) self.log.info( f"Extracted instance '{instance.name}' to: {asset_filepath}") @staticmethod def get_export_options(log_path): """Set Export Options for USD Exporter""" export_options = rt.USDExporter.createOptions() export_options.Meshes = True export_options.Shapes = False export_options.Lights = False export_options.Cameras = False export_options.Materials = False export_options.MeshFormat = rt.Name('fromScene') export_options.FileFormat = rt.Name('ascii') export_options.UpAxis = rt.Name('y') export_options.LogLevel = rt.Name('info') export_options.LogPath = log_path export_options.PreserveEdgeOrientation = True export_options.TimeMode = rt.Name('current') rt.USDexporter.UIOptions = export_options return export_options ================================================ FILE: openpype/hosts/max/plugins/publish/extract_pointcloud.py ================================================ import os import pyblish.api from pymxs import runtime as rt from openpype.hosts.max.api import maintained_selection from openpype.pipeline import publish class ExtractPointCloud(publish.Extractor): """ Extract PRT format with tyFlow operators. Notes: Currently only works for the default partition setting Args: self.export_particle(): sets up all job arguments for attributes to be exported in MAXscript self.get_operators(): get the export_particle operator self.get_custom_attr(): get all custom channel attributes from Openpype setting and sets it as job arguments before exporting self.get_files(): get the files with tyFlow naming convention before publishing self.partition_output_name(): get the naming with partition settings. self.get_partition(): get partition value """ order = pyblish.api.ExtractorOrder - 0.2 label = "Extract Point Cloud" hosts = ["max"] families = ["pointcloud"] settings = [] def process(self, instance): self.settings = self.get_setting(instance) start = instance.data["frameStartHandle"] end = instance.data["frameEndHandle"] self.log.info("Extracting PRT...") stagingdir = self.staging_dir(instance) filename = "{name}.prt".format(**instance.data) path = os.path.join(stagingdir, filename) with maintained_selection(): job_args = self.export_particle(instance.data["members"], start, end, path) for job in job_args: rt.Execute(job) self.log.info("Performing Extraction ...") if "representations" not in instance.data: instance.data["representations"] = [] self.log.info("Writing PRT with TyFlow Plugin...") filenames = self.get_files( instance.data["members"], path, start, end) self.log.debug(f"filenames: {filenames}") partition = self.partition_output_name( instance.data["members"]) representation = { 'name': 'prt', 'ext': 'prt', 'files': filenames if len(filenames) > 1 else filenames[0], "stagingDir": stagingdir, "outputName": partition # partition value } instance.data["representations"].append(representation) self.log.info(f"Extracted instance '{instance.name}' to: {path}") def export_particle(self, members, start, end, filepath): """Sets up all job arguments for attributes. Those attributes are to be exported in MAX Script. Args: members (list): Member nodes of the instance. start (int): Start frame. end (int): End frame. filepath (str): Path to PRT file. Returns: list of arguments for MAX Script. """ job_args = [] opt_list = self.get_operators(members) for operator in opt_list: start_frame = f"{operator}.frameStart={start}" job_args.append(start_frame) end_frame = f"{operator}.frameEnd={end}" job_args.append(end_frame) filepath = filepath.replace("\\", "/") prt_filename = f'{operator}.PRTFilename="{filepath}"' job_args.append(prt_filename) # Partition mode = f"{operator}.PRTPartitionsMode=2" job_args.append(mode) additional_args = self.get_custom_attr(operator) job_args.extend(iter(additional_args)) prt_export = f"{operator}.exportPRT()" job_args.append(prt_export) return job_args @staticmethod def get_operators(members): """Get Export Particles Operator. Args: members (list): Instance members. Returns: list of particle operators """ opt_list = [] for member in members: obj = member.baseobject # TODO: to see if it can be used maxscript instead anim_names = rt.GetSubAnimNames(obj) for anim_name in anim_names: sub_anim = rt.GetSubAnim(obj, anim_name) boolean = rt.IsProperty(sub_anim, "Export_Particles") if boolean: event_name = sub_anim.Name opt = f"${member.Name}.{event_name}.export_particles" opt_list.append(opt) return opt_list @staticmethod def get_setting(instance): project_setting = instance.context.data["project_settings"] return project_setting["max"]["PointCloud"] def get_custom_attr(self, operator): """Get Custom Attributes""" custom_attr_list = [] attr_settings = self.settings["attribute"] for key, value in attr_settings.items(): custom_attr = "{0}.PRTChannels_{1}=True".format(operator, value) self.log.debug( "{0} will be added as custom attribute".format(key) ) custom_attr_list.append(custom_attr) return custom_attr_list def get_files(self, container, path, start_frame, end_frame): """Get file names for tyFlow. Set the filenames accordingly to the tyFlow file naming extension for the publishing purpose Actual File Output from tyFlow:: __part of ..prt e.g. tyFlow_cloth_CCCS_blobbyFill_001__part1of1_00004.prt Args: container: Instance node. path (str): Output directory. start_frame (int): Start frame. end_frame (int): End frame. Returns: list of filenames """ filenames = [] filename = os.path.basename(path) orig_name, ext = os.path.splitext(filename) partition_count, partition_start = self.get_partition(container) for frame in range(int(start_frame), int(end_frame) + 1): actual_name = "{}__part{:03}of{}_{:05}".format(orig_name, partition_start, partition_count, frame) actual_filename = path.replace(orig_name, actual_name) filenames.append(os.path.basename(actual_filename)) return filenames def partition_output_name(self, container): """Get partition output name. Partition output name set for mapping the published file output. Todo: Customizes the setting for the output. Args: container: Instance node. Returns: str: Partition name. """ partition_count, partition_start = self.get_partition(container) return f"_part{partition_start:03}of{partition_count}" def get_partition(self, container): """Get Partition value. Args: container: Instance node. """ opt_list = self.get_operators(container) # TODO: This looks strange? Iterating over # the opt_list but returning from inside? for operator in opt_list: count = rt.Execute(f'{operator}.PRTPartitionsCount') start = rt.Execute(f'{operator}.PRTPartitionsFrom') return count, start ================================================ FILE: openpype/hosts/max/plugins/publish/extract_redshift_proxy.py ================================================ import os import pyblish.api from openpype.pipeline import publish from pymxs import runtime as rt from openpype.hosts.max.api import maintained_selection class ExtractRedshiftProxy(publish.Extractor): """ Extract Redshift Proxy with rsProxy """ order = pyblish.api.ExtractorOrder - 0.1 label = "Extract RedShift Proxy" hosts = ["max"] families = ["redshiftproxy"] def process(self, instance): start = instance.data["frameStartHandle"] end = instance.data["frameEndHandle"] self.log.debug("Extracting Redshift Proxy...") stagingdir = self.staging_dir(instance) rs_filename = "{name}.rs".format(**instance.data) rs_filepath = os.path.join(stagingdir, rs_filename) rs_filepath = rs_filepath.replace("\\", "/") rs_filenames = self.get_rsfiles(instance, start, end) with maintained_selection(): # select and export node_list = instance.data["members"] rt.Select(node_list) # Redshift rsProxy command # rsProxy fp selected compress connectivity startFrame endFrame # camera warnExisting transformPivotToOrigin rt.rsProxy(rs_filepath, 1, 0, 0, start, end, 0, 1, 1) self.log.info("Performing Extraction ...") if "representations" not in instance.data: instance.data["representations"] = [] representation = { 'name': 'rs', 'ext': 'rs', 'files': rs_filenames if len(rs_filenames) > 1 else rs_filenames[0], # noqa "stagingDir": stagingdir, } instance.data["representations"].append(representation) self.log.info("Extracted instance '%s' to: %s" % (instance.name, stagingdir)) def get_rsfiles(self, instance, startFrame, endFrame): rs_filenames = [] rs_name = instance.data["name"] for frame in range(startFrame, endFrame + 1): rs_filename = "%s.%04d.rs" % (rs_name, frame) rs_filenames.append(rs_filename) return rs_filenames ================================================ FILE: openpype/hosts/max/plugins/publish/extract_review_animation.py ================================================ import os import pyblish.api from openpype.pipeline import publish from openpype.hosts.max.api.preview_animation import ( render_preview_animation ) class ExtractReviewAnimation(publish.Extractor): """ Extract Review by Review Animation """ order = pyblish.api.ExtractorOrder + 0.001 label = "Extract Review Animation" hosts = ["max"] families = ["review"] def process(self, instance): staging_dir = self.staging_dir(instance) ext = instance.data.get("imageFormat") start = int(instance.data["frameStart"]) end = int(instance.data["frameEnd"]) filepath = os.path.join(staging_dir, instance.name) self.log.debug( "Writing Review Animation to '{}'".format(filepath)) review_camera = instance.data["review_camera"] viewport_options = instance.data.get("viewport_options", {}) files = render_preview_animation( filepath, ext, review_camera, start, end, percentSize=instance.data["percentSize"], width=instance.data["review_width"], height=instance.data["review_height"], viewport_options=viewport_options) filenames = [os.path.basename(path) for path in files] tags = ["review"] if not instance.data.get("keepImages"): tags.append("delete") self.log.debug("Performing Extraction ...") representation = { "name": instance.data["imageFormat"], "ext": instance.data["imageFormat"], "files": filenames, "stagingDir": staging_dir, "frameStart": instance.data["frameStartHandle"], "frameEnd": instance.data["frameEndHandle"], "tags": tags, "preview": True, "camera_name": review_camera } self.log.debug(f"{representation}") if "representations" not in instance.data: instance.data["representations"] = [] instance.data["representations"].append(representation) ================================================ FILE: openpype/hosts/max/plugins/publish/extract_thumbnail.py ================================================ import os import pyblish.api from openpype.pipeline import publish from openpype.hosts.max.api.preview_animation import render_preview_animation class ExtractThumbnail(publish.Extractor): """Extract Thumbnail for Review """ order = pyblish.api.ExtractorOrder label = "Extract Thumbnail" hosts = ["max"] families = ["review"] def process(self, instance): ext = instance.data.get("imageFormat") frame = int(instance.data["frameStart"]) staging_dir = self.staging_dir(instance) filepath = os.path.join( staging_dir, f"{instance.name}_thumbnail") self.log.debug("Writing Thumbnail to '{}'".format(filepath)) review_camera = instance.data["review_camera"] viewport_options = instance.data.get("viewport_options", {}) files = render_preview_animation( filepath, ext, review_camera, start_frame=frame, end_frame=frame, percentSize=instance.data["percentSize"], width=instance.data["review_width"], height=instance.data["review_height"], viewport_options=viewport_options) thumbnail = next(os.path.basename(path) for path in files) representation = { "name": "thumbnail", "ext": ext, "files": thumbnail, "stagingDir": staging_dir, "thumbnail": True } self.log.debug(f"{representation}") if "representations" not in instance.data: instance.data["representations"] = [] instance.data["representations"].append(representation) ================================================ FILE: openpype/hosts/max/plugins/publish/extract_tycache.py ================================================ import os import pyblish.api from pymxs import runtime as rt from openpype.hosts.max.api import maintained_selection from openpype.pipeline import publish class ExtractTyCache(publish.Extractor): """Extract tycache format with tyFlow operators. Notes: - TyCache only works for TyFlow Pro Plugin. Methods: self.get_export_particles_job_args(): sets up all job arguments for attributes to be exported in MAXscript self.get_operators(): get the export_particle operator self.get_files(): get the files with tyFlow naming convention before publishing """ order = pyblish.api.ExtractorOrder - 0.2 label = "Extract TyCache" hosts = ["max"] families = ["tycache"] def process(self, instance): # TODO: let user decide the param start = int(instance.context.data["frameStart"]) end = int(instance.context.data.get("frameEnd")) self.log.debug("Extracting Tycache...") stagingdir = self.staging_dir(instance) filename = "{name}.tyc".format(**instance.data) path = os.path.join(stagingdir, filename) filenames = self.get_files(instance, start, end) additional_attributes = instance.data.get("tyc_attrs", {}) with maintained_selection(): job_args = self.get_export_particles_job_args( instance.data["members"], start, end, path, additional_attributes) for job in job_args: rt.Execute(job) representations = instance.data.setdefault("representations", []) representation = { 'name': 'tyc', 'ext': 'tyc', 'files': filenames if len(filenames) > 1 else filenames[0], "stagingDir": stagingdir, } representations.append(representation) # Get the tyMesh filename for extraction mesh_filename = f"{instance.name}__tyMesh.tyc" mesh_repres = { 'name': 'tyMesh', 'ext': 'tyc', 'files': mesh_filename, "stagingDir": stagingdir, "outputName": '__tyMesh' } representations.append(mesh_repres) # Get the material filename of which assigned in # tyCache for extraction material_filename = f"{instance.name}__tyMtl.mat" full_material_name = os.path.join(stagingdir, material_filename) full_material_name = full_material_name.replace("\\", "/") if os.path.exists(full_material_name): mateiral_repres = { "name": 'tyMtl', "ext": 'mat', 'files': material_filename, 'stagingDir': stagingdir, "outputName": '__tyMtl' } representations.append(mateiral_repres) self.log.debug(f"Extracted instance '{instance.name}' to: {filenames}") def get_files(self, instance, start_frame, end_frame): """Get file names for tyFlow in tyCache format. Set the filenames accordingly to the tyCache file naming extension(.tyc) for the publishing purpose Actual File Output from tyFlow in tyCache format: __tyPart_.tyc e.g. tycacheMain__tyPart_00000.tyc Args: instance (pyblish.api.Instance): instance. start_frame (int): Start frame. end_frame (int): End frame. Returns: filenames(list): list of filenames """ filenames = [] for frame in range(int(start_frame), int(end_frame) + 1): filename = f"{instance.name}__tyPart_{frame:05}.tyc" filenames.append(filename) return filenames def get_export_particles_job_args(self, members, start, end, filepath, additional_attributes): """Sets up all job arguments for attributes. Those attributes are to be exported in MAX Script. Args: members (list): Member nodes of the instance. start (int): Start frame. end (int): End frame. filepath (str): Output path of the TyCache file. additional_attributes (dict): channel attributes data which needed to be exported Returns: list of arguments for MAX Script. """ settings = { "exportMode": 2, "frameStart": start, "frameEnd": end, "tyCacheFilename": filepath.replace("\\", "/") } settings.update(additional_attributes) job_args = [] for operator in self.get_operators(members): for key, value in settings.items(): if isinstance(value, str): # embed in quotes value = f'"{value}"' job_args.append(f"{operator}.{key}={value}") job_args.append(f"{operator}.exportTyCache()") return job_args @staticmethod def get_operators(members): """Get Export Particles Operator. Args: members (list): Instance members. Returns: list of particle operators """ opt_list = [] for member in members: obj = member.baseobject # TODO: see if it can use maxscript instead anim_names = rt.GetSubAnimNames(obj) for anim_name in anim_names: sub_anim = rt.GetSubAnim(obj, anim_name) boolean = rt.IsProperty(sub_anim, "Export_Particles") if boolean: event_name = sub_anim.Name opt = f"${member.Name}.{event_name}.export_particles" opt_list.append(opt) return opt_list ================================================ FILE: openpype/hosts/max/plugins/publish/increment_workfile_version.py ================================================ import pyblish.api from openpype.lib import version_up from pymxs import runtime as rt class IncrementWorkfileVersion(pyblish.api.ContextPlugin): """Increment current workfile version.""" order = pyblish.api.IntegratorOrder + 0.9 label = "Increment Workfile Version" hosts = ["max"] families = ["workfile"] def process(self, context): path = context.data["currentFile"] filepath = version_up(path) rt.saveMaxFile(filepath) self.log.info("Incrementing file version") ================================================ FILE: openpype/hosts/max/plugins/publish/save_scene.py ================================================ import pyblish.api from openpype.pipeline import registered_host class SaveCurrentScene(pyblish.api.ContextPlugin): """Save current scene""" label = "Save current file" order = pyblish.api.ExtractorOrder - 0.49 hosts = ["max"] families = ["maxrender", "workfile"] def process(self, context): host = registered_host() current_file = host.get_current_workfile() assert context.data["currentFile"] == current_file if host.workfile_has_unsaved_changes(): self.log.info(f"Saving current file: {current_file}") host.save_workfile(current_file) else: self.log.debug("No unsaved changes, skipping file save..") ================================================ FILE: openpype/hosts/max/plugins/publish/save_scenes_for_cameras.py ================================================ import pyblish.api import os import sys import tempfile from pymxs import runtime as rt from openpype.lib import run_subprocess from openpype.hosts.max.api.lib_rendersettings import RenderSettings from openpype.hosts.max.api.lib_renderproducts import RenderProducts class SaveScenesForCamera(pyblish.api.InstancePlugin): """Save scene files for multiple cameras without editing the original scene before deadline submission """ label = "Save Scene files for cameras" order = pyblish.api.ExtractorOrder - 0.48 hosts = ["max"] families = ["maxrender"] def process(self, instance): if not instance.data.get("multiCamera"): self.log.debug( "Multi Camera disabled. " "Skipping to save scene files for cameras") return current_folder = rt.maxFilePath current_filename = rt.maxFileName current_filepath = os.path.join(current_folder, current_filename) camera_scene_files = [] scripts = [] filename, ext = os.path.splitext(current_filename) fmt = RenderProducts().image_format() cameras = instance.data.get("cameras") if not cameras: return new_folder = f"{current_folder}_{filename}" os.makedirs(new_folder, exist_ok=True) for camera in cameras: new_output = RenderSettings().get_batch_render_output(camera) # noqa new_output = new_output.replace("\\", "/") new_filename = f"{filename}_{camera}{ext}" new_filepath = os.path.join(new_folder, new_filename) new_filepath = new_filepath.replace("\\", "/") camera_scene_files.append(new_filepath) RenderSettings().batch_render_elements(camera) rt.rendOutputFilename = new_output rt.saveMaxFile(current_filepath) script = (""" from pymxs import runtime as rt import os filename = "{filename}" new_filepath = "{new_filepath}" new_output = "{new_output}" camera = "{camera}" rt.rendOutputFilename = new_output directory = os.path.dirname(rt.rendOutputFilename) directory = os.path.join(directory, filename) render_elem = rt.maxOps.GetCurRenderElementMgr() render_elem_num = render_elem.NumRenderElements() if render_elem_num > 0: ext = "{ext}" for i in range(render_elem_num): renderlayer_name = render_elem.GetRenderElement(i) target, renderpass = str(renderlayer_name).split(":") aov_name = f"{{directory}}_{camera}_{{renderpass}}..{ext}" render_elem.SetRenderElementFileName(i, aov_name) rt.saveMaxFile(new_filepath) """).format(filename=instance.name, new_filepath=new_filepath, new_output=new_output, camera=camera, ext=fmt) scripts.append(script) maxbatch_exe = os.path.join( os.path.dirname(sys.executable), "3dsmaxbatch") maxbatch_exe = maxbatch_exe.replace("\\", "/") if sys.platform == "windows": maxbatch_exe += ".exe" maxbatch_exe = os.path.normpath(maxbatch_exe) with tempfile.TemporaryDirectory() as tmp_dir_name: tmp_script_path = os.path.join( tmp_dir_name, "extract_scene_files.py") self.log.info("Using script file: {}".format(tmp_script_path)) with open(tmp_script_path, "wt") as tmp: for script in scripts: tmp.write(script + "\n") try: current_filepath = current_filepath.replace("\\", "/") tmp_script_path = tmp_script_path.replace("\\", "/") run_subprocess([maxbatch_exe, tmp_script_path, "-sceneFile", current_filepath]) except RuntimeError: self.log.debug("Checking the scene files existing") for camera_scene in camera_scene_files: if not os.path.exists(camera_scene): self.log.error("Camera scene files not existed yet!") raise RuntimeError("MaxBatch.exe doesn't run as expected") self.log.debug(f"Found Camera scene:{camera_scene}") ================================================ FILE: openpype/hosts/max/plugins/publish/validate_attributes.py ================================================ # -*- coding: utf-8 -*- """Validator for Attributes.""" from pyblish.api import ContextPlugin, ValidatorOrder from pymxs import runtime as rt from openpype.pipeline.publish import ( OptionalPyblishPluginMixin, PublishValidationError, RepairContextAction ) def has_property(object_name, property_name): """Return whether an object has a property with given name""" return rt.Execute(f'isProperty {object_name} "{property_name}"') def is_matching_value(object_name, property_name, value): """Return whether an existing property matches value `value""" property_value = rt.Execute(f"{object_name}.{property_name}") # Wrap property value if value is a string valued attributes # starting with a `#` if ( isinstance(value, str) and value.startswith("#") and not value.endswith(")") ): # prefix value with `#` # not applicable for #() array value type # and only applicable for enum i.e. #bob, #sally property_value = f"#{property_value}" return property_value == value class ValidateAttributes(OptionalPyblishPluginMixin, ContextPlugin): """Validates attributes in the project setting are consistent with the nodes from MaxWrapper Class in 3ds max. E.g. "renderers.current.separateAovFiles", "renderers.production.PrimaryGIEngine" Admin(s) need to put the dict below and enable this validator for a check: { "renderers.current":{ "separateAovFiles" : True }, "renderers.production":{ "PrimaryGIEngine": "#RS_GIENGINE_BRUTE_FORCE" } .... } """ order = ValidatorOrder hosts = ["max"] label = "Attributes" actions = [RepairContextAction] optional = True @classmethod def get_invalid(cls, context): attributes = ( context.data["project_settings"]["max"]["publish"] ["ValidateAttributes"]["attributes"] ) if not attributes: return invalid = [] for object_name, required_properties in attributes.items(): if not rt.Execute(f"isValidValue {object_name}"): # Skip checking if the node does not # exist in MaxWrapper Class cls.log.debug(f"Unable to find '{object_name}'." " Skipping validation of attributes.") continue for property_name, value in required_properties.items(): if not has_property(object_name, property_name): cls.log.error( "Non-existing property: " f"{object_name}.{property_name}") invalid.append((object_name, property_name)) if not is_matching_value(object_name, property_name, value): cls.log.error( f"Invalid value for: {object_name}.{property_name}" f" should be: {value}") invalid.append((object_name, property_name)) return invalid def process(self, context): if not self.is_active(context.data): self.log.debug("Skipping Validate Attributes...") return invalid_attributes = self.get_invalid(context) if invalid_attributes: bullet_point_invalid_statement = "\n".join( "- {}".format(invalid) for invalid in invalid_attributes ) report = ( "Required Attribute(s) have invalid value(s).\n\n" f"{bullet_point_invalid_statement}\n\n" "You can use repair action to fix them if they are not\n" "unknown property value(s)." ) raise PublishValidationError( report, title="Invalid Value(s) for Required Attribute(s)") @classmethod def repair(cls, context): attributes = ( context.data["project_settings"]["max"]["publish"] ["ValidateAttributes"]["attributes"] ) invalid_attributes = cls.get_invalid(context) for attrs in invalid_attributes: prop, attr = attrs value = attributes[prop][attr] if isinstance(value, str) and not value.startswith("#"): attribute_fix = '{}.{}="{}"'.format( prop, attr, value ) else: attribute_fix = "{}.{}={}".format( prop, attr, value ) rt.Execute(attribute_fix) ================================================ FILE: openpype/hosts/max/plugins/publish/validate_camera_attributes.py ================================================ import pyblish.api from pymxs import runtime as rt from openpype.pipeline.publish import ( RepairAction, OptionalPyblishPluginMixin, PublishValidationError ) from openpype.hosts.max.api.action import SelectInvalidAction class ValidateCameraAttributes(OptionalPyblishPluginMixin, pyblish.api.InstancePlugin): """Validates Camera has no invalid attribute properties or values.(For 3dsMax Cameras only) """ order = pyblish.api.ValidatorOrder families = ['camera'] hosts = ['max'] label = 'Validate Camera Attributes' actions = [SelectInvalidAction, RepairAction] optional = True DEFAULTS = ["fov", "nearrange", "farrange", "nearclip", "farclip"] CAM_TYPE = ["Freecamera", "Targetcamera", "Physical"] @classmethod def get_invalid(cls, instance): invalid = [] if rt.units.DisplayType != rt.Name("Generic"): cls.log.warning( "Generic Type is not used as a scene unit\n\n" "sure you tweak the settings with your own values\n\n" "before validation.") cameras = instance.data["members"] project_settings = instance.context.data["project_settings"].get("max") cam_attr_settings = ( project_settings["publish"]["ValidateCameraAttributes"] ) for camera in cameras: if str(rt.ClassOf(camera)) not in cls.CAM_TYPE: cls.log.debug( "Skipping camera created from external plugin..") continue for attr in cls.DEFAULTS: default_value = cam_attr_settings.get(attr) if default_value == float(0): cls.log.debug( f"the value of {attr} in setting set to" " zero. Skipping the check.") continue if round(rt.getProperty(camera, attr), 1) != default_value: cls.log.error( f"Invalid attribute value for {camera.name}:{attr} " f"(should be: {default_value}))") invalid.append(camera) return invalid def process(self, instance): if not self.is_active(instance.data): self.log.debug("Skipping Validate Camera Attributes.") return invalid = self.get_invalid(instance) if invalid: raise PublishValidationError( "Invalid camera attributes found. See log.") @classmethod def repair(cls, instance): invalid_cameras = cls.get_invalid(instance) project_settings = instance.context.data["project_settings"].get("max") cam_attr_settings = ( project_settings["publish"]["ValidateCameraAttributes"] ) for camera in invalid_cameras: for attr in cls.DEFAULTS: expected_value = cam_attr_settings.get(attr) if expected_value == float(0): cls.log.debug( f"the value of {attr} in setting set to zero.") continue rt.setProperty(camera, attr, expected_value) ================================================ FILE: openpype/hosts/max/plugins/publish/validate_camera_contents.py ================================================ # -*- coding: utf-8 -*- import pyblish.api from openpype.pipeline import PublishValidationError from pymxs import runtime as rt class ValidateCameraContent(pyblish.api.InstancePlugin): """Validates Camera instance contents. A Camera instance may only hold a SINGLE camera's transform """ order = pyblish.api.ValidatorOrder families = ["camera", "review"] hosts = ["max"] label = "Camera Contents" camera_type = ["$Free_Camera", "$Target_Camera", "$Physical_Camera", "$Target"] def process(self, instance): invalid = self.get_invalid(instance) if invalid: raise PublishValidationError(("Camera instance must only include" "camera (and camera target). " f"Invalid content {invalid}")) def get_invalid(self, instance): """ Get invalid nodes if the instance is not camera """ invalid = [] container = instance.data["instance_node"] self.log.info(f"Validating camera content for {container}") selection_list = instance.data["members"] for sel in selection_list: # to avoid Attribute Error from pymxs wrapper sel_tmp = str(sel) found = any(sel_tmp.startswith(cam) for cam in self.camera_type) if not found: self.log.error("Camera not found") invalid.append(sel) return invalid ================================================ FILE: openpype/hosts/max/plugins/publish/validate_frame_range.py ================================================ import pyblish.api from pymxs import runtime as rt from openpype.pipeline import ( OptionalPyblishPluginMixin ) from openpype.pipeline.publish import ( RepairAction, ValidateContentsOrder, PublishValidationError, KnownPublishError ) from openpype.hosts.max.api.lib import get_frame_range, set_timeline class ValidateFrameRange(pyblish.api.InstancePlugin, OptionalPyblishPluginMixin): """Validates the frame ranges. This is an optional validator checking if the frame range on instance matches the frame range specified for the asset. It also validates render frame ranges of render layers. Repair action will change everything to match the asset frame range. This can be turned off by the artist to allow custom ranges. """ label = "Validate Frame Range" order = ValidateContentsOrder families = ["camera", "maxrender", "pointcache", "pointcloud", "review", "redshiftproxy"] hosts = ["max"] optional = True actions = [RepairAction] def process(self, instance): if not self.is_active(instance.data): self.log.debug("Skipping Validate Frame Range...") return frame_range = get_frame_range( asset_doc=instance.data["assetEntity"]) inst_frame_start = instance.data.get("frameStartHandle") inst_frame_end = instance.data.get("frameEndHandle") if inst_frame_start is None or inst_frame_end is None: raise KnownPublishError( "Missing frame start and frame end on " "instance to to validate." ) frame_start_handle = frame_range["frameStartHandle"] frame_end_handle = frame_range["frameEndHandle"] errors = [] if frame_start_handle != inst_frame_start: errors.append( f"Start frame ({inst_frame_start}) on instance does not match " # noqa f"with the start frame ({frame_start_handle}) set on the asset data. ") # noqa if frame_end_handle != inst_frame_end: errors.append( f"End frame ({inst_frame_end}) on instance does not match " f"with the end frame ({frame_end_handle}) " "from the asset data. ") if errors: bullet_point_errors = "\n".join( "- {}".format(error) for error in errors ) report = ( "Frame range settings are incorrect.\n\n" f"{bullet_point_errors}\n\n" "You can use repair action to fix it." ) raise PublishValidationError(report, title="Frame Range incorrect") @classmethod def repair(cls, instance): frame_range = get_frame_range() frame_start_handle = frame_range["frameStartHandle"] frame_end_handle = frame_range["frameEndHandle"] if instance.data["family"] == "maxrender": rt.rendStart = frame_start_handle rt.rendEnd = frame_end_handle else: set_timeline(frame_start_handle, frame_end_handle) ================================================ FILE: openpype/hosts/max/plugins/publish/validate_instance_has_members.py ================================================ # -*- coding: utf-8 -*- import pyblish.api from openpype.pipeline import PublishValidationError class ValidateInstanceHasMembers(pyblish.api.InstancePlugin): """Validates Instance has members. Check if MaxScene containers includes any contents underneath. """ order = pyblish.api.ValidatorOrder families = ["camera", "model", "maxScene", "review", "pointcache", "pointcloud", "redshiftproxy"] hosts = ["max"] label = "Container Contents" def process(self, instance): if not instance.data["members"]: raise PublishValidationError("No content found in the container") ================================================ FILE: openpype/hosts/max/plugins/publish/validate_instance_in_context.py ================================================ # -*- coding: utf-8 -*- """Validate if instance asset is the same as context asset.""" from __future__ import absolute_import import pyblish.api from openpype import AYON_SERVER_ENABLED from openpype.pipeline.publish import ( RepairAction, ValidateContentsOrder, PublishValidationError, OptionalPyblishPluginMixin ) from openpype.hosts.max.api.action import SelectInvalidAction from pymxs import runtime as rt class ValidateInstanceInContext(pyblish.api.InstancePlugin, OptionalPyblishPluginMixin): """Validator to check if instance asset match context asset. When working in per-shot style you always publish data in context of current asset (shot). This validator checks if this is so. It is optional so it can be disabled when needed. Action on this validator will select invalid instances. """ order = ValidateContentsOrder label = "Instance in same Context" optional = True hosts = ["max"] actions = [SelectInvalidAction, RepairAction] def process(self, instance): if not self.is_active(instance.data): return instance_node = rt.getNodeByName(instance.data.get( "instance_node", "")) if not instance_node: return asset_name_attr = "folderPath" if AYON_SERVER_ENABLED else "asset" asset = rt.getUserProp(instance_node, asset_name_attr) context_asset = self.get_context_asset(instance) task = rt.getUserProp(instance_node, "task") context_task = self.get_context_task(instance) if asset != context_asset: raise PublishValidationError( message=( "Instance '{}' publishes to different asset than current " "context: {}. Current context: {}".format( instance.name, asset, context_asset ) ), description=( "## Publishing to a different asset\n" "There are publish instances present which are publishing " "into a different asset than your current context.\n\n" "Usually this is not what you want but there can be cases " "where you might want to publish into another asset or " "shot. If that's the case you can disable the validation " "on the instance to ignore it." ) ) if task != context_task: raise PublishValidationError( message=( "Instance '{}' publishes to different task than current " "context: {}. Current context: {}".format( instance.name, task, context_task ) ), description=( "## Publishing to a different asset\n" "There are publish instances present which are publishing " "into a different asset than your current context.\n\n" "Usually this is not what you want but there can be cases " "where you might want to publish into another asset or " "shot. If that's the case you can disable the validation " "on the instance to ignore it." ) ) @classmethod def get_invalid(cls, instance): asset_name_attr = "folderPath" if AYON_SERVER_ENABLED else "asset" node = rt.getNodeByName(instance.data["instance_node"]) asset = rt.getUserProp(node, asset_name_attr) context_asset = cls.get_context_asset(instance) if asset != context_asset: return instance.data["instance_node"] @classmethod def repair(cls, instance): context_asset = cls.get_context_asset(instance) context_task = cls.get_context_task(instance) instance_node = rt.getNodeByName(instance.data.get( "instance_node", "")) if not instance_node: return asset_name_attr = "folderPath" if AYON_SERVER_ENABLED else "asset" rt.SetUserProp(instance_node, asset_name_attr, context_asset) rt.SetUserProp(instance_node, "task", context_task) @staticmethod def get_context_asset(instance): return instance.context.data["asset"] @staticmethod def get_context_task(instance): return instance.context.data["task"] ================================================ FILE: openpype/hosts/max/plugins/publish/validate_loaded_plugin.py ================================================ # -*- coding: utf-8 -*- """Validator for Loaded Plugin.""" import os import pyblish.api from pymxs import runtime as rt from openpype.pipeline.publish import ( RepairAction, OptionalPyblishPluginMixin, PublishValidationError ) from openpype.hosts.max.api.lib import get_plugins class ValidateLoadedPlugin(OptionalPyblishPluginMixin, pyblish.api.InstancePlugin): """Validates if the specific plugin is loaded in 3ds max. Studio Admin(s) can add the plugins they want to check in validation via studio defined project settings """ order = pyblish.api.ValidatorOrder hosts = ["max"] label = "Validate Loaded Plugins" optional = True actions = [RepairAction] family_plugins_mapping = {} @classmethod def get_invalid(cls, instance): """Plugin entry point.""" family_plugins_mapping = cls.family_plugins_mapping if not family_plugins_mapping: return invalid = [] # Find all plug-in requirements for current instance instance_families = {instance.data["family"]} instance_families.update(instance.data.get("families", [])) cls.log.debug("Checking plug-in validation " f"for instance families: {instance_families}") all_required_plugins = set() for mapping in family_plugins_mapping: # Check for matching families if not mapping: return match_families = {fam.strip() for fam in mapping["families"]} has_match = "*" in match_families or match_families.intersection( instance_families) if not has_match: continue cls.log.debug( f"Found plug-in family requirements: {match_families}") required_plugins = [ # match lowercase and format with os.environ to allow # plugin names defined by max version, e.g. {3DSMAX_VERSION} plugin.format(**os.environ).lower() for plugin in mapping["plugins"] # ignore empty fields in settings if plugin.strip() ] all_required_plugins.update(required_plugins) if not all_required_plugins: # Instance has no plug-in requirements return # get all DLL loaded plugins in Max and their plugin index available_plugins = { plugin_name.lower(): index for index, plugin_name in enumerate( get_plugins()) } # validate the required plug-ins for plugin in sorted(all_required_plugins): plugin_index = available_plugins.get(plugin) if plugin_index is None: debug_msg = ( f"Plugin {plugin} does not exist" " in 3dsMax Plugin List." ) invalid.append((plugin, debug_msg)) continue if not rt.pluginManager.isPluginDllLoaded(plugin_index): debug_msg = f"Plugin {plugin} not loaded." invalid.append((plugin, debug_msg)) return invalid def process(self, instance): if not self.is_active(instance.data): self.log.debug("Skipping Validate Loaded Plugin...") return invalid = self.get_invalid(instance) if invalid: bullet_point_invalid_statement = "\n".join( "- {}".format(message) for _, message in invalid ) report = ( "Required plugins are not loaded.\n\n" f"{bullet_point_invalid_statement}\n\n" "You can use repair action to load the plugin." ) raise PublishValidationError( report, title="Missing Required Plugins") @classmethod def repair(cls, instance): # get all DLL loaded plugins in Max and their plugin index invalid = cls.get_invalid(instance) if not invalid: return # get all DLL loaded plugins in Max and their plugin index available_plugins = { plugin_name.lower(): index for index, plugin_name in enumerate( get_plugins()) } for invalid_plugin, _ in invalid: plugin_index = available_plugins.get(invalid_plugin) if plugin_index is None: cls.log.warning( f"Can't enable missing plugin: {invalid_plugin}") continue if not rt.pluginManager.isPluginDllLoaded(plugin_index): rt.pluginManager.loadPluginDll(plugin_index) ================================================ FILE: openpype/hosts/max/plugins/publish/validate_model_contents.py ================================================ # -*- coding: utf-8 -*- import pyblish.api from pymxs import runtime as rt from openpype.pipeline import PublishValidationError class ValidateModelContent(pyblish.api.InstancePlugin): """Validates Model instance contents. A model instance may only hold either geometry-related object(excluding Shapes) or editable meshes. """ order = pyblish.api.ValidatorOrder families = ["model"] hosts = ["max"] label = "Model Contents" def process(self, instance): invalid = self.get_invalid(instance) if invalid: raise PublishValidationError(("Model instance must only include" "Geometry and Editable Mesh. " f"Invalid types on: {invalid}")) def get_invalid(self, instance): """ Get invalid nodes if the instance is not camera """ invalid = [] container = instance.data["instance_node"] self.log.info(f"Validating model content for {container}") selection_list = instance.data["members"] for sel in selection_list: if rt.ClassOf(sel) in rt.Camera.classes: invalid.append(sel) if rt.ClassOf(sel) in rt.Light.classes: invalid.append(sel) if rt.ClassOf(sel) in rt.Shape.classes: invalid.append(sel) return invalid ================================================ FILE: openpype/hosts/max/plugins/publish/validate_pointcloud.py ================================================ import pyblish.api from openpype.pipeline import PublishValidationError from pymxs import runtime as rt class ValidatePointCloud(pyblish.api.InstancePlugin): """Validate that work file was saved.""" order = pyblish.api.ValidatorOrder families = ["pointcloud"] hosts = ["max"] label = "Validate Point Cloud" def process(self, instance): """ Notes: 1. Validate if the export mode of Export Particle is at PRT format 2. Validate the partition count and range set as default value Partition Count : 100 Partition Range : 1 to 1 3. Validate if the custom attribute(s) exist as parameter(s) of export_particle operator """ report = [] if self.validate_export_mode(instance): report.append("The export mode is not at PRT") if self.validate_partition_value(instance): report.append(("tyFlow Partition setting is " "not at the default value")) invalid_attribute = self.validate_custom_attribute(instance) if invalid_attribute: report.append(("Custom Attribute not found " f":{invalid_attribute}")) if report: raise PublishValidationError(f"{report}") def validate_custom_attribute(self, instance): invalid = [] container = instance.data["instance_node"] self.log.info( f"Validating tyFlow custom attributes for {container}") selection_list = instance.data["members"] project_settings = instance.context.data["project_settings"] attr_settings = project_settings["max"]["PointCloud"]["attribute"] for sel in selection_list: obj = sel.baseobject anim_names = rt.GetSubAnimNames(obj) for anim_name in anim_names: # get all the names of the related tyFlow nodes sub_anim = rt.GetSubAnim(obj, anim_name) if rt.IsProperty(sub_anim, "Export_Particles"): event_name = sub_anim.name opt = "${0}.{1}.export_particles".format(sel.name, event_name) for key, value in attr_settings.items(): custom_attr = "{0}.PRTChannels_{1}".format(opt, value) try: rt.Execute(custom_attr) except RuntimeError: invalid.append(key) return invalid def validate_partition_value(self, instance): invalid = [] container = instance.data["instance_node"] self.log.info( f"Validating tyFlow partition value for {container}") selection_list = instance.data["members"] for sel in selection_list: obj = sel.baseobject anim_names = rt.GetSubAnimNames(obj) for anim_name in anim_names: # get all the names of the related tyFlow nodes sub_anim = rt.GetSubAnim(obj, anim_name) if rt.IsProperty(sub_anim, "Export_Particles"): event_name = sub_anim.name opt = "${0}.{1}.export_particles".format(sel.name, event_name) count = rt.Execute(f'{opt}.PRTPartitionsCount') if count != 100: invalid.append(count) start = rt.Execute(f'{opt}.PRTPartitionsFrom') if start != 1: invalid.append(start) end = rt.Execute(f'{opt}.PRTPartitionsTo') if end != 1: invalid.append(end) return invalid def validate_export_mode(self, instance): invalid = [] container = instance.data["instance_node"] self.log.info( f"Validating tyFlow export mode for {container}") con = rt.GetNodeByName(container) selection_list = list(con.Children) for sel in selection_list: obj = sel.baseobject anim_names = rt.GetSubAnimNames(obj) for anim_name in anim_names: # get all the names of the related tyFlow nodes sub_anim = rt.GetSubAnim(obj, anim_name) # check if there is export particle operator boolean = rt.IsProperty(sub_anim, "Export_Particles") event_name = sub_anim.name if boolean: opt = f"${sel.name}.{event_name}.export_particles" export_mode = rt.Execute(f'{opt}.exportMode') if export_mode != 1: invalid.append(export_mode) return invalid ================================================ FILE: openpype/hosts/max/plugins/publish/validate_renderable_camera.py ================================================ # -*- coding: utf-8 -*- import pyblish.api from openpype.pipeline import ( PublishValidationError, OptionalPyblishPluginMixin) from openpype.pipeline.publish import RepairAction from openpype.hosts.max.api.lib import get_current_renderer from pymxs import runtime as rt class ValidateRenderableCamera(pyblish.api.InstancePlugin, OptionalPyblishPluginMixin): """Validates Renderable Camera Check if the renderable camera used for rendering """ order = pyblish.api.ValidatorOrder families = ["maxrender"] hosts = ["max"] label = "Renderable Camera" optional = True actions = [RepairAction] def process(self, instance): if not self.is_active(instance.data): return if not instance.data["cameras"]: raise PublishValidationError( "No renderable Camera found in scene." ) @classmethod def repair(cls, instance): rt.viewport.setType(rt.Name("view_camera")) camera = rt.viewport.GetCamera() cls.log.info(f"Camera {camera} set as renderable camera") renderer_class = get_current_renderer() renderer = str(renderer_class).split(":")[0] if renderer == "Arnold": arv = rt.MAXToAOps.ArnoldRenderView() arv.setOption("Camera", str(camera)) arv.close() instance.data["cameras"] = [camera.name] ================================================ FILE: openpype/hosts/max/plugins/publish/validate_renderer_redshift_proxy.py ================================================ # -*- coding: utf-8 -*- import pyblish.api from openpype.pipeline import PublishValidationError from pymxs import runtime as rt from openpype.pipeline.publish import RepairAction from openpype.hosts.max.api.lib import get_current_renderer class ValidateRendererRedshiftProxy(pyblish.api.InstancePlugin): """ Validates Redshift as the current renderer for creating Redshift Proxy """ order = pyblish.api.ValidatorOrder families = ["redshiftproxy"] hosts = ["max"] label = "Redshift Renderer" actions = [RepairAction] def process(self, instance): invalid = self.get_redshift_renderer(instance) if invalid: raise PublishValidationError("Please install Redshift for 3dsMax" " before using the Redshift proxy instance") # noqa invalid = self.get_current_renderer(instance) if invalid: raise PublishValidationError("The Redshift proxy extraction" "discontinued since the current renderer is not Redshift") # noqa def get_redshift_renderer(self, instance): invalid = list() max_renderers_list = str(rt.RendererClass.classes) if "Redshift_Renderer" not in max_renderers_list: invalid.append(max_renderers_list) return invalid def get_current_renderer(self, instance): invalid = list() renderer_class = get_current_renderer() current_renderer = str(renderer_class).split(":")[0] if current_renderer != "Redshift_Renderer": invalid.append(current_renderer) return invalid @classmethod def repair(cls, instance): for Renderer in rt.RendererClass.classes: renderer = Renderer() if "Redshift_Renderer" in str(renderer): rt.renderers.production = renderer break ================================================ FILE: openpype/hosts/max/plugins/publish/validate_renderpasses.py ================================================ import os import pyblish.api from pymxs import runtime as rt from openpype.pipeline.publish import ( RepairAction, ValidateContentsOrder, PublishValidationError, OptionalPyblishPluginMixin ) from openpype.hosts.max.api.lib_rendersettings import RenderSettings class ValidateRenderPasses(OptionalPyblishPluginMixin, pyblish.api.InstancePlugin): """Validates Render Passes before Deadline Submission """ order = ValidateContentsOrder families = ["maxrender"] hosts = ["max"] label = "Validate Render Passes" optional = True actions = [RepairAction] def process(self, instance): if not self.is_active(instance.data): return invalid = self.get_invalid(instance) if invalid: bullet_point_invalid_statement = "\n".join( f"- {err_type}: {filepath}" for err_type, filepath in invalid ) report = ( "Invalid render passes found.\n\n" f"{bullet_point_invalid_statement}\n\n" "You can use repair action to fix the invalid filepath." ) raise PublishValidationError( report, title="Invalid Render Passes") @classmethod def get_invalid(cls, instance): """Function to get invalid beauty render outputs and render elements. 1. Check Render Output Folder matches the name of the current Max Scene, e.g. The name of the current Max scene: John_Doe.max The expected render output directory: {root[work]}/{project[name]}/{hierarchy}/{asset}/ work/{task[name]}/render/3dsmax/John_Doe/ 2. Check image extension(s) of the render output(s) matches the image format in OP/AYON setting, e.g. The current image format in settings: png The expected render outputs: John_Doe.png 3. Check filename of render element ends with the name of render element from the 3dsMax Render Element Manager. e.g. The name of render element: RsCryptomatte The expected filename: {InstanceName}_RsCryptomatte.png Args: instance (pyblish.api.Instance): instance filename (str): filename of the Max scene Returns: list: list of invalid filename which doesn't match with the project name """ invalid = [] file = rt.maxFileName filename, ext = os.path.splitext(file) if filename not in rt.rendOutputFilename: cls.log.error( "Render output folder must include " f" the max scene name {filename} " ) invalid_folder_name = os.path.dirname( rt.rendOutputFilename).replace( "\\", "/").split("/")[-1] invalid.append(("Invalid Render Output Folder", invalid_folder_name)) beauty_fname = os.path.basename(rt.rendOutputFilename) beauty_name, ext = os.path.splitext(beauty_fname) invalid_filenames = cls.get_invalid_filenames( instance, beauty_name) invalid.extend(invalid_filenames) invalid_image_format = cls.get_invalid_image_format( instance, ext.lstrip(".")) invalid.extend(invalid_image_format) renderer = instance.data["renderer"] if renderer in [ "ART_Renderer", "Redshift_Renderer", "V_Ray_6_Hotfix_3", "V_Ray_GPU_6_Hotfix_3", "Default_Scanline_Renderer", "Quicksilver_Hardware_Renderer", ]: render_elem = rt.maxOps.GetCurRenderElementMgr() render_elem_num = render_elem.NumRenderElements() for i in range(render_elem_num): renderlayer_name = render_elem.GetRenderElement(i) renderpass = str(renderlayer_name).split(":")[-1] rend_file = render_elem.GetRenderElementFilename(i) if not rend_file: cls.log.error( f"No filepath for render element {renderpass}") invalid.append((f"Invalid {renderpass}", "No filepath")) rend_fname, ext = os.path.splitext( os.path.basename(rend_file)) invalid_filenames = cls.get_invalid_filenames( instance, rend_fname, renderpass=renderpass) invalid.extend(invalid_filenames) invalid_image_format = cls.get_invalid_image_format( instance, ext) invalid.extend(invalid_image_format) elif renderer == "Arnold": cls.log.debug( "Renderpass validation does not support Arnold yet," " validation skipped...") return invalid @classmethod def get_invalid_filenames(cls, instance, file_name, renderpass=None): """Function to get invalid filenames from render outputs. Args: instance (pyblish.api.Instance): instance file_name (str): name of the file renderpass (str, optional): name of the renderpass. Defaults to None. Returns: list: invalid filenames """ invalid = [] if instance.name not in file_name: cls.log.error("The renderpass should have instance name inside.") invalid.append((f"Invalid instance name", file_name)) if renderpass is not None: if not file_name.rstrip(".").endswith(renderpass): cls.log.error( f"Filename for {renderpass} should " f"end with {renderpass}" ) invalid.append((f"Invalid {renderpass}", os.path.basename(file_name))) return invalid @classmethod def get_invalid_image_format(cls, instance, ext): """Function to check if the image format of the render outputs aligns with that in the setting. Args: instance (pyblish.api.Instance): instance ext (str): image extension Returns: list: list of files with invalid image format """ invalid = [] settings = instance.context.data["project_settings"].get("max") image_format = settings["RenderSettings"]["image_format"] ext = ext.lstrip(".") if ext != image_format: msg = ( f"Invalid image format {ext} for render outputs.\n" f"Should be: {image_format}") cls.log.error(msg) invalid.append((msg, ext)) return invalid @classmethod def repair(cls, instance): container = instance.data.get("instance_node") # TODO: need to rename the function of render_output RenderSettings().render_output(container) cls.log.debug("Finished repairing the render output " "folder and filenames.") ================================================ FILE: openpype/hosts/max/plugins/publish/validate_resolution_setting.py ================================================ import pyblish.api from pymxs import runtime as rt from openpype.pipeline import ( OptionalPyblishPluginMixin ) from openpype.pipeline.publish import ( RepairAction, PublishValidationError ) from openpype.hosts.max.api.lib import reset_scene_resolution class ValidateResolutionSetting(pyblish.api.InstancePlugin, OptionalPyblishPluginMixin): """Validate the resolution setting aligned with DB""" order = pyblish.api.ValidatorOrder - 0.01 families = ["maxrender"] hosts = ["max"] label = "Validate Resolution Setting" optional = True actions = [RepairAction] def process(self, instance): if not self.is_active(instance.data): return width, height = self.get_db_resolution(instance) current_width = rt.renderWidth current_height = rt.renderHeight if current_width != width and current_height != height: raise PublishValidationError("Resolution Setting " "not matching resolution " "set on asset or shot.") if current_width != width: raise PublishValidationError("Width in Resolution Setting " "not matching resolution set " "on asset or shot.") if current_height != height: raise PublishValidationError("Height in Resolution Setting " "not matching resolution set " "on asset or shot.") def get_db_resolution(self, instance): asset_doc = instance.data["assetEntity"] project_doc = instance.context.data["projectEntity"] for data in [asset_doc["data"], project_doc["data"]]: if "resolutionWidth" in data and "resolutionHeight" in data: width = data["resolutionWidth"] height = data["resolutionHeight"] return int(width), int(height) # Defaults if not found in asset document or project document return 1920, 1080 @classmethod def repair(cls, instance): reset_scene_resolution() ================================================ FILE: openpype/hosts/max/plugins/publish/validate_scene_saved.py ================================================ # -*- coding: utf-8 -*- import pyblish.api from openpype.pipeline import PublishValidationError from pymxs import runtime as rt class ValidateSceneSaved(pyblish.api.InstancePlugin): """Validate that workfile was saved.""" order = pyblish.api.ValidatorOrder families = ["workfile"] hosts = ["max"] label = "Validate Workfile is saved" def process(self, instance): if not rt.maxFilePath or not rt.maxFileName: raise PublishValidationError( "Workfile is not saved", title=self.label) ================================================ FILE: openpype/hosts/max/plugins/publish/validate_tyflow_data.py ================================================ import pyblish.api from openpype.pipeline import PublishValidationError from pymxs import runtime as rt class ValidateTyFlowData(pyblish.api.InstancePlugin): """Validate TyFlow plugins or relevant operators are set correctly.""" order = pyblish.api.ValidatorOrder families = ["pointcloud", "tycache"] hosts = ["max"] label = "TyFlow Data" def process(self, instance): """ Notes: 1. Validate the container only include tyFlow objects 2. Validate if tyFlow operator Export Particle exists """ invalid_object = self.get_tyflow_object(instance) if invalid_object: self.log.error(f"Non tyFlow object found: {invalid_object}") invalid_operator = self.get_tyflow_operator(instance) if invalid_operator: self.log.error( "Operator 'Export Particles' not found in tyFlow editor.") if invalid_object or invalid_operator: raise PublishValidationError( "issues occurred", description="Container should only include tyFlow object " "and tyflow operator 'Export Particle' should be in " "the tyFlow editor.") def get_tyflow_object(self, instance): """Get the nodes which are not tyFlow object(s) and editable mesh(es) Args: instance (pyblish.api.Instance): instance Returns: list: invalid nodes which are not tyFlow object(s) and editable mesh(es). """ container = instance.data["instance_node"] self.log.debug(f"Validating tyFlow container for {container}") allowed_classes = [rt.tyFlow, rt.Editable_Mesh] return [ member for member in instance.data["members"] if rt.ClassOf(member) not in allowed_classes ] def get_tyflow_operator(self, instance): """Check if the Export Particle Operators in the node connections. Args: instance (str): instance node Returns: invalid(list): list of invalid nodes which do not consist of Export Particle Operators as parts of the node connections """ invalid = [] members = instance.data["members"] for member in members: obj = member.baseobject # There must be at least one animation with export # particles enabled has_export_particles = False anim_names = rt.GetSubAnimNames(obj) for anim_name in anim_names: # get name of the related tyFlow node sub_anim = rt.GetSubAnim(obj, anim_name) # check if there is export particle operator if rt.IsProperty(sub_anim, "Export_Particles"): has_export_particles = True break if not has_export_particles: invalid.append(member) return invalid ================================================ FILE: openpype/hosts/max/startup/startup.ms ================================================ -- OpenPype Init Script ( local sysPath = dotNetClass "System.IO.Path" local sysDir = dotNetClass "System.IO.Directory" local localScript = getThisScriptFilename() local startup = sysPath.Combine (sysPath.GetDirectoryName localScript) "startup.py" local pythonpath = systemTools.getEnvVariable "MAX_PYTHONPATH" systemTools.setEnvVariable "PYTHONPATH" pythonpath /*opens the create menu on startup to ensure users are presented with a useful default view.*/ max create mode python.ExecuteFile startup ) ================================================ FILE: openpype/hosts/max/startup/startup.py ================================================ # -*- coding: utf-8 -*- import os import sys # this might happen in some 3dsmax version where PYTHONPATH isn't added # to sys.path automatically for path in os.environ["PYTHONPATH"].split(os.pathsep): if path and path not in sys.path: sys.path.append(path) from openpype.hosts.max.api import MaxHost from openpype.pipeline import install_host host = MaxHost() install_host(host) ================================================ FILE: openpype/hosts/maya/__init__.py ================================================ from .addon import ( MayaAddon, MAYA_ROOT_DIR, ) __all__ = ( "MayaAddon", "MAYA_ROOT_DIR", ) ================================================ FILE: openpype/hosts/maya/addon.py ================================================ import os from openpype.modules import OpenPypeModule, IHostAddon MAYA_ROOT_DIR = os.path.dirname(os.path.abspath(__file__)) class MayaAddon(OpenPypeModule, IHostAddon): name = "maya" host_name = "maya" def initialize(self, module_settings): self.enabled = True def add_implementation_envs(self, env, _app): # Add requirements to PYTHONPATH new_python_paths = [ os.path.join(MAYA_ROOT_DIR, "startup") ] old_python_path = env.get("PYTHONPATH") or "" for path in old_python_path.split(os.pathsep): if not path: continue norm_path = os.path.normpath(path) if norm_path not in new_python_paths: new_python_paths.append(norm_path) env["PYTHONPATH"] = os.pathsep.join(new_python_paths) # Set default environments envs = { "OPENPYPE_LOG_NO_COLORS": "Yes", # For python module 'qtpy' "QT_API": "PySide2", # For python module 'Qt' "QT_PREFERRED_BINDING": "PySide2" } for key, value in envs.items(): env[key] = value def get_launch_hook_paths(self, app): if app.host_name != self.host_name: return [] return [ os.path.join(MAYA_ROOT_DIR, "hooks") ] def get_workfile_extensions(self): return [".ma", ".mb"] ================================================ FILE: openpype/hosts/maya/api/__init__.py ================================================ """Public API Anything that isn't defined here is INTERNAL and unreliable for external use. """ from .pipeline import ( uninstall, ls, containerise, MayaHost, ) from .plugin import ( Creator, Loader ) from .workio import ( open_file, save_file, current_file, has_unsaved_changes, file_extensions, work_root ) from .lib import ( lsattr, lsattrs, read, apply_shaders, maintained_selection, suspended_refresh, unique_namespace, ) __all__ = [ "uninstall", "ls", "containerise", "MayaHost", "Creator", "Loader", # Workfiles API "open_file", "save_file", "current_file", "has_unsaved_changes", "file_extensions", "work_root", # Utility functions "lsattr", "lsattrs", "read", "unique_namespace", "apply_shaders", "maintained_selection", "suspended_refresh", ] # Backwards API compatibility open = open_file save = save_file ================================================ FILE: openpype/hosts/maya/api/action.py ================================================ # absolute_import is needed to counter the `module has no cmds error` in Maya from __future__ import absolute_import import pyblish.api from openpype.client import get_asset_by_name from openpype.pipeline.publish import get_errored_instances_from_context class GenerateUUIDsOnInvalidAction(pyblish.api.Action): """Generate UUIDs on the invalid nodes in the instance. Invalid nodes are those returned by the plugin's `get_invalid` method. As such it is the plug-in's responsibility to ensure the nodes that receive new UUIDs are actually invalid. Requires: - instance.data["asset"] """ label = "Regenerate UUIDs" on = "failed" # This action is only available on a failed plug-in icon = "wrench" # Icon from Awesome Icon def process(self, context, plugin): from maya import cmds self.log.info("Finding bad nodes..") errored_instances = get_errored_instances_from_context(context) # Apply pyblish logic to get the instances for the plug-in instances = pyblish.api.instances_by_plugin(errored_instances, plugin) # Get the nodes from the all instances that ran through this plug-in all_invalid = [] for instance in instances: invalid = plugin.get_invalid(instance) # Don't allow referenced nodes to get their ids regenerated to # avoid loaded content getting messed up with reference edits if invalid: referenced = {node for node in invalid if cmds.referenceQuery(node, isNodeReferenced=True)} if referenced: self.log.warning("Skipping UUID generation on referenced " "nodes: {}".format(list(referenced))) invalid = [node for node in invalid if node not in referenced] if invalid: self.log.info("Fixing instance {}".format(instance.name)) self._update_id_attribute(instance, invalid) all_invalid.extend(invalid) if not all_invalid: self.log.info("No invalid nodes found.") return all_invalid = list(set(all_invalid)) self.log.info("Generated ids on nodes: {0}".format(all_invalid)) def _update_id_attribute(self, instance, nodes): """Delete the id attribute Args: instance: The instance we're fixing for nodes (list): all nodes to regenerate ids on """ from . import lib # Expecting this is called on validators in which case 'assetEntity' # should be always available, but kept a way to query it by name. asset_doc = instance.data.get("assetEntity") if not asset_doc: asset_name = instance.data["asset"] project_name = instance.context.data["projectName"] self.log.info(( "Asset is not stored on instance." " Querying by name \"{}\" from project \"{}\"" ).format(asset_name, project_name)) asset_doc = get_asset_by_name( project_name, asset_name, fields=["_id"] ) for node, _id in lib.generate_ids(nodes, asset_id=asset_doc["_id"]): lib.set_id(node, _id, overwrite=True) class SelectInvalidAction(pyblish.api.Action): """Select invalid nodes in Maya when plug-in failed. To retrieve the invalid nodes this assumes a static `get_invalid()` method is available on the plugin. """ label = "Select invalid" on = "failed" # This action is only available on a failed plug-in icon = "search" # Icon from Awesome Icon def process(self, context, plugin): try: from maya import cmds except ImportError: raise ImportError("Current host is not Maya") errored_instances = get_errored_instances_from_context(context, plugin=plugin) # Get the invalid nodes for the plug-ins self.log.info("Finding invalid nodes..") invalid = list() for instance in errored_instances: invalid_nodes = plugin.get_invalid(instance) if invalid_nodes: if isinstance(invalid_nodes, (list, tuple)): invalid.extend(invalid_nodes) else: self.log.warning("Plug-in returned to be invalid, " "but has no selectable nodes.") # Ensure unique (process each node only once) invalid = list(set(invalid)) if invalid: self.log.info("Selecting invalid nodes: %s" % ", ".join(invalid)) cmds.select(invalid, replace=True, noExpand=True) else: self.log.info("No invalid nodes found.") cmds.select(deselect=True) ================================================ FILE: openpype/hosts/maya/api/alembic.py ================================================ import json import logging import os from maya import cmds # noqa from openpype.hosts.maya.api.lib import evaluation log = logging.getLogger(__name__) # The maya alembic export types ALEMBIC_ARGS = { "attr": (list, tuple), "attrPrefix": (list, tuple), "autoSubd": bool, "dataFormat": str, "dontSkipUnwrittenFrames": bool, "endFrame": float, "eulerFilter": bool, "frameRange": str, # "start end"; overrides startFrame & endFrame "frameRelativeSample": float, "melPerFrameCallback": str, "melPostJobCallback": str, "noNormals": bool, "preRoll": bool, "preRollStartFrame": int, "pythonPerFrameCallback": str, "pythonPostJobCallback": str, "renderableOnly": bool, "root": (list, tuple), "selection": bool, "startFrame": float, "step": float, "stripNamespaces": bool, "userAttr": (list, tuple), "userAttrPrefix": (list, tuple), "uvWrite": bool, "uvsOnly": bool, "verbose": bool, "wholeFrameGeo": bool, "worldSpace": bool, "writeColorSets": bool, "writeCreases": bool, # Maya 2015 Ext1+ "writeFaceSets": bool, "writeUVSets": bool, # Maya 2017+ "writeVisibility": bool, } def extract_alembic( file, attr=None, attrPrefix=None, dataFormat="ogawa", endFrame=None, eulerFilter=True, frameRange="", noNormals=False, preRoll=False, preRollStartFrame=0, renderableOnly=False, root=None, selection=True, startFrame=None, step=1.0, stripNamespaces=True, uvWrite=True, verbose=False, wholeFrameGeo=False, worldSpace=False, writeColorSets=False, writeCreases=False, writeNormals=False, writeFaceSets=False, writeUVSets=False, writeVisibility=False ): """Extract a single Alembic Cache. This extracts an Alembic cache using the `-selection` flag to minimize the extracted content to solely what was Collected into the instance. Arguments: startFrame (float): Start frame of output. Ignored if `frameRange` provided. endFrame (float): End frame of output. Ignored if `frameRange` provided. frameRange (tuple or str): Two-tuple with start and end frame or a string formatted as: "startFrame endFrame". This argument overrides `startFrame` and `endFrame` arguments. eulerFilter (bool): When on, X, Y, and Z rotation data is filtered with an Euler filter. Euler filtering helps resolve irregularities in rotations especially if X, Y, and Z rotations exceed 360 degrees. Defaults to True. noNormals (bool): When on, normal data from the original polygon objects is not included in the exported Alembic cache file. preRoll (bool): This frame range will not be sampled. Defaults to False. renderableOnly (bool): When on, any non-renderable nodes or hierarchy, such as hidden objects, are not included in the Alembic file. Defaults to False. selection (bool): Write out all all selected nodes from the active selection list that are descendents of the roots specified with -root. Defaults to False. uvWrite (bool): When on, UV data from polygon meshes and subdivision objects are written to the Alembic file. Only the current UV map is included. writeColorSets (bool): Write all color sets on MFnMeshes as color 3 or color 4 indexed geometry parameters with face varying scope. Defaults to False. writeFaceSets (bool): Write all Face sets on MFnMeshes. Defaults to False. wholeFrameGeo (bool): Data for geometry will only be written out on whole frames. Defaults to False. worldSpace (bool): When on, the top node in the node hierarchy is stored as world space. By default, these nodes are stored as local space. Defaults to False. writeVisibility (bool): Visibility state will be stored in the Alembic file. Otherwise everything written out is treated as visible. Defaults to False. writeUVSets (bool): Write all uv sets on MFnMeshes as vector 2 indexed geometry parameters with face varying scope. Defaults to False. writeCreases (bool): If the mesh has crease edges or crease vertices, the mesh (OPolyMesh) would now be written out as an OSubD and crease info will be stored in the Alembic file. Otherwise, creases info won't be preserved in Alembic file unless a custom Boolean attribute SubDivisionMesh has been added to mesh node and its value is true. Defaults to False. dataFormat (str): The data format to use for the cache, defaults to "ogawa" step (float): The time interval (expressed in frames) at which the frame range is sampled. Additional samples around each frame can be specified with -frs. Defaults to 1.0. attr (list of str, optional): A specific geometric attribute to write out. Defaults to []. attrPrefix (list of str, optional): Prefix filter for determining which geometric attributes to write out. Defaults to ["ABC_"]. root (list of str): Maya dag path which will be parented to the root of the Alembic file. Defaults to [], which means the entire scene will be written out. stripNamespaces (bool): When on, any namespaces associated with the exported objects are removed from the Alembic file. For example, an object with the namespace taco:foo:bar appears as bar in the Alembic file. verbose (bool): When on, outputs frame number information to the Script Editor or output window during extraction. preRollStartFrame (float): The frame to start scene evaluation at. This is used to set the starting frame for time dependent translations and can be used to evaluate run-up that isn't actually translated. Defaults to 0. """ # Ensure alembic exporter is loaded cmds.loadPlugin('AbcExport', quiet=True) # Alembic Exporter requires forward slashes file = file.replace('\\', '/') # Ensure list arguments are valid. attr = attr or [] attrPrefix = attrPrefix or [] root = root or [] # Pass the start and end frame on as `frameRange` so that it # never conflicts with that argument if not frameRange: # Fallback to maya timeline if no start or end frame provided. if startFrame is None: startFrame = cmds.playbackOptions(query=True, animationStartTime=True) if endFrame is None: endFrame = cmds.playbackOptions(query=True, animationEndTime=True) # Ensure valid types are converted to frame range assert isinstance(startFrame, ALEMBIC_ARGS["startFrame"]) assert isinstance(endFrame, ALEMBIC_ARGS["endFrame"]) frameRange = "{0} {1}".format(startFrame, endFrame) else: # Allow conversion from tuple for `frameRange` if isinstance(frameRange, (list, tuple)): assert len(frameRange) == 2 frameRange = "{0} {1}".format(frameRange[0], frameRange[1]) # Assemble options options = { "selection": selection, "frameRange": frameRange, "eulerFilter": eulerFilter, "noNormals": noNormals, "preRoll": preRoll, "renderableOnly": renderableOnly, "uvWrite": uvWrite, "writeColorSets": writeColorSets, "writeFaceSets": writeFaceSets, "wholeFrameGeo": wholeFrameGeo, "worldSpace": worldSpace, "writeVisibility": writeVisibility, "writeUVSets": writeUVSets, "writeCreases": writeCreases, "dataFormat": dataFormat, "step": step, "attr": attr, "attrPrefix": attrPrefix, "stripNamespaces": stripNamespaces, "verbose": verbose, "preRollStartFrame": preRollStartFrame } # Validate options for key, value in options.copy().items(): # Discard unknown options if key not in ALEMBIC_ARGS: log.warning("extract_alembic() does not support option '%s'. " "Flag will be ignored..", key) options.pop(key) continue # Validate value type valid_types = ALEMBIC_ARGS[key] if not isinstance(value, valid_types): raise TypeError("Alembic option unsupported type: " "{0} (expected {1})".format(value, valid_types)) # Ignore empty values, like an empty string, since they mess up how # job arguments are built if isinstance(value, (list, tuple)): value = [x for x in value if x.strip()] # Ignore option completely if no values remaining if not value: options.pop(key) continue options[key] = value # The `writeCreases` argument was changed to `autoSubd` in Maya 2018+ maya_version = int(cmds.about(version=True)) if maya_version >= 2018: options['autoSubd'] = options.pop('writeCreases', False) # Format the job string from options job_args = list() for key, value in options.items(): if isinstance(value, (list, tuple)): for entry in value: job_args.append("-{} {}".format(key, entry)) elif isinstance(value, bool): # Add only when state is set to True if value: job_args.append("-{0}".format(key)) else: job_args.append("-{0} {1}".format(key, value)) job_str = " ".join(job_args) job_str += ' -file "%s"' % file # Ensure output directory exists parent_dir = os.path.dirname(file) if not os.path.exists(parent_dir): os.makedirs(parent_dir) if verbose: log.debug("Preparing Alembic export with options: %s", json.dumps(options, indent=4)) log.debug("Extracting Alembic with job arguments: %s", job_str) # Perform extraction print("Alembic Job Arguments : {}".format(job_str)) # Disable the parallel evaluation temporarily to ensure no buggy # exports are made. (PLN-31) # TODO: Make sure this actually fixes the issues with evaluation("off"): cmds.AbcExport(j=job_str, verbose=verbose) if verbose: log.debug("Extracted Alembic to: %s", file) return file ================================================ FILE: openpype/hosts/maya/api/commands.py ================================================ # -*- coding: utf-8 -*- """OpenPype script commands to be used directly in Maya.""" from maya import cmds from openpype.client import get_asset_by_name, get_project from openpype.pipeline import get_current_project_name, get_current_asset_name class ToolWindows: _windows = {} @classmethod def get_window(cls, tool): """Get widget for specific tool. Args: tool (str): Name of the tool. Returns: Stored widget. """ try: return cls._windows[tool] except KeyError: return None @classmethod def set_window(cls, tool, window): """Set widget for the tool. Args: tool (str): Name of the tool. window (QtWidgets.QWidget): Widget """ cls._windows[tool] = window def edit_shader_definitions(): from qtpy import QtWidgets from openpype.hosts.maya.api.shader_definition_editor import ( ShaderDefinitionsEditor ) from openpype.tools.utils import qt_app_context top_level_widgets = QtWidgets.QApplication.topLevelWidgets() main_window = next(widget for widget in top_level_widgets if widget.objectName() == "MayaWindow") with qt_app_context(): window = ToolWindows.get_window("shader_definition_editor") if not window: window = ShaderDefinitionsEditor(parent=main_window) ToolWindows.set_window("shader_definition_editor", window) window.show() def _resolution_from_document(doc): if not doc or "data" not in doc: print("Entered document is not valid. \"{}\"".format(str(doc))) return None resolution_width = doc["data"].get("resolutionWidth") resolution_height = doc["data"].get("resolutionHeight") # Backwards compatibility if resolution_width is None or resolution_height is None: resolution_width = doc["data"].get("resolution_width") resolution_height = doc["data"].get("resolution_height") # Make sure both width and height are set if resolution_width is None or resolution_height is None: cmds.warning( "No resolution information found for \"{}\"".format(doc["name"]) ) return None return int(resolution_width), int(resolution_height) def reset_resolution(): # Default values resolution_width = 1920 resolution_height = 1080 # Get resolution from asset project_name = get_current_project_name() asset_name = get_current_asset_name() asset_doc = get_asset_by_name(project_name, asset_name) resolution = _resolution_from_document(asset_doc) # Try get resolution from project if resolution is None: # TODO go through visualParents print(( "Asset \"{}\" does not have set resolution." " Trying to get resolution from project" ).format(asset_name)) project_doc = get_project(project_name) resolution = _resolution_from_document(project_doc) if resolution is None: msg = "Using default resolution {}x{}" else: resolution_width, resolution_height = resolution msg = "Setting resolution to {}x{}" print(msg.format(resolution_width, resolution_height)) # set for different renderers # arnold, vray, redshift, renderman renderer = cmds.getAttr("defaultRenderGlobals.currentRenderer").lower() # handle various renderman names if renderer.startswith("renderman"): renderer = "renderman" # default attributes are usable for Arnold, Renderman and Redshift width_attr_name = "defaultResolution.width" height_attr_name = "defaultResolution.height" # Vray has its own way if renderer == "vray": width_attr_name = "vraySettings.width" height_attr_name = "vraySettings.height" cmds.setAttr(width_attr_name, resolution_width) cmds.setAttr(height_attr_name, resolution_height) ================================================ FILE: openpype/hosts/maya/api/customize.py ================================================ """A set of commands that install overrides to Maya's UI""" import os import logging from functools import partial import maya.cmds as cmds import maya.mel as mel from openpype import resources from openpype.tools.utils import host_tools from .lib import get_main_window from ..tools import show_look_assigner log = logging.getLogger(__name__) COMPONENT_MASK_ORIGINAL = {} def override_component_mask_commands(): """Override component mask ctrl+click behavior. This implements special behavior for Maya's component mask menu items where a ctrl+click will instantly make it an isolated behavior disabling all others. Tested in Maya 2016 and 2018 """ log.info("Installing override_component_mask_commands..") # Get all object mask buttons buttons = cmds.formLayout("objectMaskIcons", query=True, childArray=True) # Skip the triangle list item buttons = [btn for btn in buttons if btn != "objPickMenuLayout"] def on_changed_callback(raw_command, state): """New callback""" # If "control" is held force the toggled one to on and # toggle the others based on whether any of the buttons # was remaining active after the toggle, if not then # enable all if cmds.getModifiers() == 4: # = CTRL state = True active = [cmds.iconTextCheckBox(btn, query=True, value=True) for btn in buttons] if any(active): cmds.selectType(allObjects=False) else: cmds.selectType(allObjects=True) # Replace #1 with the current button state cmd = raw_command.replace(" #1", " {}".format(int(state))) mel.eval(cmd) for btn in buttons: # Store a reference to the original command so that if # we rerun this override command it doesn't recursively # try to implement the fix. (This also allows us to # "uninstall" the behavior later) if btn not in COMPONENT_MASK_ORIGINAL: original = cmds.iconTextCheckBox(btn, query=True, cc=True) COMPONENT_MASK_ORIGINAL[btn] = original # Assign the special callback original = COMPONENT_MASK_ORIGINAL[btn] new_fn = partial(on_changed_callback, original) cmds.iconTextCheckBox(btn, edit=True, cc=new_fn) def override_toolbox_ui(): """Add custom buttons in Toolbox as replacement for Maya web help icon.""" icons = resources.get_resource("icons") parent_widget = get_main_window() # Ensure the maya web icon on toolbox exists button_names = [ # Maya 2022.1+ with maya.cmds.iconTextStaticLabel "ToolBox|MainToolboxLayout|mayaHomeToolboxButton", # Older with maya.cmds.iconTextButton "ToolBox|MainToolboxLayout|mayaWebButton" ] for name in button_names: if cmds.control(name, query=True, exists=True): web_button = name break else: # Button does not exist log.warning("Can't find Maya Home/Web button to override toolbox ui..") return cmds.control(web_button, edit=True, visible=False) # real = 32, but 36 with padding - according to toolbox mel script icon_size = 36 parent = web_button.rsplit("|", 1)[0] # Ensure the parent is a formLayout if not cmds.objectTypeUI(parent) == "formLayout": return # Create our controls controls = [] controls.append( cmds.iconTextButton( "pype_toolbox_lookmanager", annotation="Look Manager", label="Look Manager", image=os.path.join(icons, "lookmanager.png"), command=show_look_assigner, width=icon_size, height=icon_size, parent=parent ) ) controls.append( cmds.iconTextButton( "pype_toolbox_workfiles", annotation="Work Files", label="Work Files", image=os.path.join(icons, "workfiles.png"), command=lambda: host_tools.show_workfiles( parent=parent_widget ), width=icon_size, height=icon_size, parent=parent ) ) controls.append( cmds.iconTextButton( "pype_toolbox_loader", annotation="Loader", label="Loader", image=os.path.join(icons, "loader.png"), command=lambda: host_tools.show_loader( parent=parent_widget, use_context=True ), width=icon_size, height=icon_size, parent=parent ) ) controls.append( cmds.iconTextButton( "pype_toolbox_manager", annotation="Inventory", label="Inventory", image=os.path.join(icons, "inventory.png"), command=lambda: host_tools.show_scene_inventory( parent=parent_widget ), width=icon_size, height=icon_size, parent=parent ) ) # Add the buttons on the bottom and stack # them above each other with side padding controls.reverse() for i, control in enumerate(controls): previous = controls[i - 1] if i > 0 else web_button cmds.formLayout(parent, edit=True, attachControl=[control, "bottom", 0, previous], attachForm=([control, "left", 1], [control, "right", 1])) ================================================ FILE: openpype/hosts/maya/api/exitstack.py ================================================ """Backwards compatible implementation of ExitStack for Python 2. ExitStack contextmanager was implemented with Python 3.3. As long as we supportPython 2 hosts we can use this backwards compatible implementation to support bothPython 2 and Python 3. Instead of using ExitStack from contextlib, use it from this module: >>> from openpype.hosts.maya.api.exitstack import ExitStack It will provide the appropriate ExitStack implementation for the current running Python version. """ # TODO: Remove the entire script once dropping Python 2 support. import contextlib if getattr(contextlib, "nested", None): from contextlib import ExitStack # noqa else: import sys from collections import deque class ExitStack(object): """Context manager for dynamic management of a stack of exit callbacks For example: with ExitStack() as stack: files = [stack.enter_context(open(fname)) for fname in filenames] # All opened files will automatically be closed at the end of # the with statement, even if attempts to open files later # in the list raise an exception """ def __init__(self): self._exit_callbacks = deque() def pop_all(self): """Preserve the context stack by transferring it to a new instance""" new_stack = type(self)() new_stack._exit_callbacks = self._exit_callbacks self._exit_callbacks = deque() return new_stack def _push_cm_exit(self, cm, cm_exit): """Helper to correctly register callbacks to __exit__ methods""" def _exit_wrapper(*exc_details): return cm_exit(cm, *exc_details) _exit_wrapper.__self__ = cm self.push(_exit_wrapper) def push(self, exit): """Registers a callback with the standard __exit__ method signature Can suppress exceptions the same way __exit__ methods can. Also accepts any object with an __exit__ method (registering a call to the method instead of the object itself) """ # We use an unbound method rather than a bound method to follow # the standard lookup behaviour for special methods _cb_type = type(exit) try: exit_method = _cb_type.__exit__ except AttributeError: # Not a context manager, so assume its a callable self._exit_callbacks.append(exit) else: self._push_cm_exit(exit, exit_method) return exit # Allow use as a decorator def callback(self, callback, *args, **kwds): """Registers an arbitrary callback and arguments. Cannot suppress exceptions. """ def _exit_wrapper(exc_type, exc, tb): callback(*args, **kwds) # We changed the signature, so using @wraps is not appropriate, but # setting __wrapped__ may still help with introspection _exit_wrapper.__wrapped__ = callback self.push(_exit_wrapper) return callback # Allow use as a decorator def enter_context(self, cm): """Enters the supplied context manager If successful, also pushes its __exit__ method as a callback and returns the result of the __enter__ method. """ # We look up the special methods on the type to # match the with statement _cm_type = type(cm) _exit = _cm_type.__exit__ result = _cm_type.__enter__(cm) self._push_cm_exit(cm, _exit) return result def close(self): """Immediately unwind the context stack""" self.__exit__(None, None, None) def __enter__(self): return self def __exit__(self, *exc_details): # We manipulate the exception state so it behaves as though # we were actually nesting multiple with statements frame_exc = sys.exc_info()[1] def _fix_exception_context(new_exc, old_exc): while 1: exc_context = new_exc.__context__ if exc_context in (None, frame_exc): break new_exc = exc_context new_exc.__context__ = old_exc # Callbacks are invoked in LIFO order to match the behaviour of # nested context managers suppressed_exc = False while self._exit_callbacks: cb = self._exit_callbacks.pop() try: if cb(*exc_details): suppressed_exc = True exc_details = (None, None, None) except Exception: new_exc_details = sys.exc_info() # simulate the stack of exceptions by setting the context _fix_exception_context(new_exc_details[1], exc_details[1]) if not self._exit_callbacks: raise exc_details = new_exc_details return suppressed_exc ================================================ FILE: openpype/hosts/maya/api/fbx.py ================================================ # -*- coding: utf-8 -*- """Tools to work with FBX.""" import logging from pyblish.api import Instance from maya import cmds # noqa import maya.mel as mel # noqa from openpype.hosts.maya.api.lib import maintained_selection class FBXExtractor: """Extract FBX from Maya. This extracts reproducible FBX exports ignoring any of the settings set on the local machine in the FBX export options window. All export settings are applied with the `FBXExport*` commands prior to the `FBXExport` call itself. The options can be overridden with their nice names as seen in the "options" property on this class. For more information on FBX exports see: - https://knowledge.autodesk.com/support/maya/learn-explore/caas /CloudHelp/cloudhelp/2016/ENU/Maya/files/GUID-6CCE943A-2ED4-4CEE-96D4 -9CB19C28F4E0-htm.html - http://forums.cgsociety.org/archive/index.php?t-1032853.html - https://groups.google.com/forum/#!msg/python_inside_maya/cLkaSo361oE /LKs9hakE28kJ """ @property def options(self): """Overridable options for FBX Export Given in the following format - {NAME: EXPECTED TYPE} If the overridden option's type does not match, the option is not included and a warning is logged. """ return { "cameras": bool, "smoothingGroups": bool, "hardEdges": bool, "tangents": bool, "smoothMesh": bool, "instances": bool, # "referencedContainersContent": bool, # deprecated in Maya 2016+ "bakeComplexAnimation": int, "bakeComplexStart": int, "bakeComplexEnd": int, "bakeComplexStep": int, "bakeResampleAnimation": bool, "useSceneName": bool, "quaternion": str, # "euler" "shapes": bool, "skins": bool, "constraints": bool, "lights": bool, "embeddedTextures": bool, "inputConnections": bool, "upAxis": str, # x, y or z, "triangulate": bool, "fileVersion": str, "skeletonDefinitions": bool, "referencedAssetsContent": bool } @property def default_options(self): """The default options for FBX extraction. This includes shapes, skins, constraints, lights and incoming connections and exports with the Y-axis as up-axis. By default this uses the time sliders start and end time. """ start_frame = int(cmds.playbackOptions(query=True, animationStartTime=True)) end_frame = int(cmds.playbackOptions(query=True, animationEndTime=True)) return { "cameras": False, "smoothingGroups": True, "hardEdges": False, "tangents": False, "smoothMesh": True, "instances": False, "bakeComplexAnimation": True, "bakeComplexStart": start_frame, "bakeComplexEnd": end_frame, "bakeComplexStep": 1, "bakeResampleAnimation": True, "useSceneName": False, "quaternion": "euler", "shapes": True, "skins": True, "constraints": False, "lights": True, "embeddedTextures": False, "inputConnections": True, "upAxis": "y", "triangulate": False, "fileVersion": "FBX202000", "skeletonDefinitions": False, "referencedAssetsContent": False } def __init__(self, log=None): # Ensure FBX plug-in is loaded self.log = log or logging.getLogger(self.__class__.__name__) cmds.loadPlugin("fbxmaya", quiet=True) def parse_overrides(self, instance, options): """Inspect data of instance to determine overridden options An instance may supply any of the overridable options as data, the option is then added to the extraction. """ for key in instance.data: if key not in self.options: continue # Ensure the data is of correct type value = instance.data[key] if not isinstance(value, self.options[key]): self.log.warning( "Overridden attribute {key} was of " "the wrong type: {invalid_type} " "- should have been {valid_type}".format( key=key, invalid_type=type(value).__name__, valid_type=self.options[key].__name__)) continue options[key] = value return options def set_options_from_instance(self, instance): # type: (Instance) -> None """Sets FBX export options from data in the instance. Args: instance (Instance): Instance data. """ # Parse export options options = self.default_options options = self.parse_overrides(instance, options) self.log.debug("Export options: {0}".format(options)) # Collect the start and end including handles start = instance.data.get("frameStartHandle") or \ instance.context.data.get("frameStartHandle") end = instance.data.get("frameEndHandle") or \ instance.context.data.get("frameEndHandle") options['bakeComplexStart'] = start options['bakeComplexEnd'] = end # First apply the default export settings to be fully consistent # each time for successive publishes mel.eval("FBXResetExport") # Apply the FBX overrides through MEL since the commands # only work correctly in MEL according to online # available discussions on the topic _iteritems = getattr(options, "iteritems", options.items) for option, value in _iteritems(): key = option[0].upper() + option[1:] # uppercase first letter # Boolean must be passed as lower-case strings # as to MEL standards if isinstance(value, bool): value = str(value).lower() template = "FBXExport{0} {1}" if key == "UpAxis" else \ "FBXExport{0} -v {1}" # noqa cmd = template.format(key, value) self.log.debug(cmd) mel.eval(cmd) # Never show the UI or generate a log mel.eval("FBXExportShowUI -v false") mel.eval("FBXExportGenerateLog -v false") @staticmethod def export(members, path): # type: (list, str) -> None """Export members as FBX with given path. Args: members (list): List of members to export. path (str): Path to use for export. """ # The export requires forward slashes because we need # to format it into a string in a mel expression path = path.replace("\\", "/") with maintained_selection(): cmds.select(members, r=True, noExpand=True) mel.eval('FBXExport -f "{}" -s'.format(path)) ================================================ FILE: openpype/hosts/maya/api/gltf.py ================================================ # -*- coding: utf-8 -*- """Tools to work with GLTF.""" import logging from maya import cmds, mel # noqa log = logging.getLogger(__name__) _gltf_options = { "of": str, # outputFolder "cpr": str, # copyright "sno": bool, # selectedNodeOnly "sn": str, # sceneName "glb": bool, # binary "nbu": bool, # niceBufferURIs "hbu": bool, # hashBufferURI "ext": bool, # externalTextures "ivt": int, # initialValuesTime "acn": str, # animationClipName "ast": int, # animationClipStartTime "aet": int, # animationClipEndTime "afr": float, # animationClipFrameRate "dsa": int, # detectStepAnimations "mpa": str, # meshPrimitiveAttributes "bpa": str, # blendPrimitiveAttributes "i32": bool, # force32bitIndices "ssm": bool, # skipStandardMaterials "eut": bool, # excludeUnusedTexcoord "dm": bool, # defaultMaterial "cm": bool, # colorizeMaterials "dmy": str, # dumpMaya "dgl": str, # dumpGLTF "imd": str, # ignoreMeshDeformers "ssc": bool, # skipSkinClusters "sbs": bool, # skipBlendShapes "rvp": bool, # redrawViewport "vno": bool # visibleNodesOnly } def extract_gltf(parent_dir, filename, **kwargs): """Sets GLTF export options from data in the instance. """ cmds.loadPlugin('maya2glTF', quiet=True) # load the UI to run mel command mel.eval("maya2glTF_UI()") parent_dir = parent_dir.replace('\\', '/') options = { "dsa": 1, "glb": True } options.update(kwargs) for key, value in options.copy().items(): if key not in _gltf_options: log.warning("extract_gltf() does not support option '%s'. " "Flag will be ignored..", key) options.pop(key) options.pop(value) continue job_args = list() default_opt = "maya2glTF -of \"{0}\" -sn \"{1}\"".format(parent_dir, filename) # noqa job_args.append(default_opt) for key, value in options.items(): if isinstance(value, str): job_args.append("-{0} \"{1}\"".format(key, value)) elif isinstance(value, bool): if value: job_args.append("-{0}".format(key)) else: job_args.append("-{0} {1}".format(key, value)) job_str = " ".join(job_args) log.info("{}".format(job_str)) mel.eval(job_str) # close the gltf export after finish the export gltf_UI = "maya2glTF_exporter_window" if cmds.window(gltf_UI, q=True, exists=True): cmds.deleteUI(gltf_UI) ================================================ FILE: openpype/hosts/maya/api/lib.py ================================================ """Standalone helper functions""" import os import copy from pprint import pformat import sys import uuid import re import json import logging import contextlib import capture from .exitstack import ExitStack from collections import OrderedDict, defaultdict from math import ceil from six import string_types from maya import cmds, mel from maya.api import OpenMaya from openpype.client import ( get_project, get_asset_by_name, get_subsets, get_last_versions, get_representation_by_name ) from openpype.settings import get_project_settings from openpype.pipeline import ( get_current_project_name, get_current_asset_name, get_current_task_name, discover_loader_plugins, loaders_from_representation, get_representation_path, load_container, registered_host ) from openpype.lib import NumberDef from openpype.pipeline.context_tools import get_current_project_asset from openpype.pipeline.create import CreateContext from openpype.lib.profiles_filtering import filter_profiles self = sys.modules[__name__] self._parent = None log = logging.getLogger(__name__) IS_HEADLESS = not hasattr(cmds, "about") or cmds.about(batch=True) ATTRIBUTE_DICT = {"int": {"attributeType": "long"}, "str": {"dataType": "string"}, "unicode": {"dataType": "string"}, "float": {"attributeType": "double"}, "bool": {"attributeType": "bool"}} SHAPE_ATTRS = {"castsShadows", "receiveShadows", "motionBlur", "primaryVisibility", "smoothShading", "visibleInReflections", "visibleInRefractions", "doubleSided", "opposite"} DEFAULT_MATRIX = [1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0] INT_FPS = {15, 24, 25, 30, 48, 50, 60, 44100, 48000} FLOAT_FPS = {23.98, 23.976, 29.97, 47.952, 59.94} DISPLAY_LIGHTS_ENUM = [ {"label": "Use Project Settings", "value": "project_settings"}, {"label": "Default Lighting", "value": "default"}, {"label": "All Lights", "value": "all"}, {"label": "Selected Lights", "value": "selected"}, {"label": "Flat Lighting", "value": "flat"}, {"label": "No Lights", "value": "none"} ] def get_main_window(): """Acquire Maya's main window""" from qtpy import QtWidgets if self._parent is None: self._parent = { widget.objectName(): widget for widget in QtWidgets.QApplication.topLevelWidgets() }["MayaWindow"] return self._parent @contextlib.contextmanager def suspended_refresh(suspend=True): """Suspend viewport refreshes cmds.ogs(pause=True) is a toggle so we cant pass False. """ if IS_HEADLESS: yield return original_state = cmds.ogs(query=True, pause=True) try: if suspend and not original_state: cmds.ogs(pause=True) yield finally: if suspend and not original_state: cmds.ogs(pause=True) @contextlib.contextmanager def maintained_selection(): """Maintain selection during context Example: >>> scene = cmds.file(new=True, force=True) >>> node = cmds.createNode("transform", name="Test") >>> cmds.select("persp") >>> with maintained_selection(): ... cmds.select("Test", replace=True) >>> "Test" in cmds.ls(selection=True) False """ previous_selection = cmds.ls(selection=True) try: yield finally: if previous_selection: cmds.select(previous_selection, replace=True, noExpand=True) else: cmds.select(clear=True) def reload_all_udim_tile_previews(): """Regenerate all UDIM tile preview in texture file""" for texture_file in cmds.ls(type="file"): if cmds.getAttr("{}.uvTilingMode".format(texture_file)) > 0: cmds.ogs(regenerateUVTilePreview=texture_file) @contextlib.contextmanager def panel_camera(panel, camera): """Set modelPanel's camera during the context. Arguments: panel (str): modelPanel name. camera (str): camera name. """ original_camera = cmds.modelPanel(panel, query=True, camera=True) try: cmds.modelPanel(panel, edit=True, camera=camera) yield finally: cmds.modelPanel(panel, edit=True, camera=original_camera) def render_capture_preset(preset): """Capture playblast with a preset. To generate the preset use `generate_capture_preset`. Args: preset (dict): preset options Returns: str: Output path of `capture.capture` """ # Force a refresh at the start of the timeline # TODO (Question): Why do we need to do this? What bug does it solve? # Is this for simulations? cmds.refresh(force=True) refresh_frame_int = int(cmds.playbackOptions(query=True, minTime=True)) cmds.currentTime(refresh_frame_int - 1, edit=True) cmds.currentTime(refresh_frame_int, edit=True) log.debug( "Using preset: {}".format( json.dumps(preset, indent=4, sort_keys=True) ) ) preset = copy.deepcopy(preset) # not supported by `capture` so we pop it off of the preset reload_textures = preset["viewport_options"].pop("loadTextures", False) panel = preset.pop("panel") with ExitStack() as stack: stack.enter_context(maintained_time()) stack.enter_context(panel_camera(panel, preset["camera"])) stack.enter_context(viewport_default_options(panel, preset)) if reload_textures: # Force immediate texture loading when to ensure # all textures have loaded before the playblast starts stack.enter_context(material_loading_mode(mode="immediate")) # Regenerate all UDIM tiles previews reload_all_udim_tile_previews() path = capture.capture(log=self.log, **preset) return path def generate_capture_preset(instance, camera, path, start=None, end=None, capture_preset=None): """Function for getting all the data of preset options for playblast capturing Args: instance (pyblish.api.Instance): instance camera (str): review camera path (str): filepath start (int): frameStart end (int): frameEnd capture_preset (dict): capture preset Returns: dict: Resulting preset """ preset = load_capture_preset(data=capture_preset) preset["camera"] = camera preset["start_frame"] = start preset["end_frame"] = end preset["filename"] = path preset["overwrite"] = True preset["panel"] = instance.data["panel"] # Disable viewer since we use the rendering logic for publishing # We don't want to open the generated playblast in a viewer directly. preset["viewer"] = False # "isolate_view" will already have been applied at creation, so we'll # ignore it here. preset.pop("isolate_view") # Set resolution variables from capture presets width_preset = capture_preset["Resolution"]["width"] height_preset = capture_preset["Resolution"]["height"] # Set resolution variables from asset values asset_data = instance.data["assetEntity"]["data"] asset_width = asset_data.get("resolutionWidth") asset_height = asset_data.get("resolutionHeight") review_instance_width = instance.data.get("review_width") review_instance_height = instance.data.get("review_height") # Use resolution from instance if review width/height is set # Otherwise use the resolution from preset if it has non-zero values # Otherwise fall back to asset width x height # Else define no width, then `capture.capture` will use render resolution if review_instance_width and review_instance_height: preset["width"] = review_instance_width preset["height"] = review_instance_height elif width_preset and height_preset: preset["width"] = width_preset preset["height"] = height_preset elif asset_width and asset_height: preset["width"] = asset_width preset["height"] = asset_height # Isolate view is requested by having objects in the set besides a # camera. If there is only 1 member it'll be the camera because we # validate to have 1 camera only. if instance.data["isolate"] and len(instance.data["setMembers"]) > 1: preset["isolate"] = instance.data["setMembers"] # Override camera options # Enforce persisting camera depth of field camera_options = preset.setdefault("camera_options", {}) camera_options["depthOfField"] = cmds.getAttr( "{0}.depthOfField".format(camera) ) # Use Pan/Zoom from instance data instead of from preset preset.pop("pan_zoom", None) camera_options["panZoomEnabled"] = instance.data["panZoom"] # Override viewport options by instance data viewport_options = preset.setdefault("viewport_options", {}) viewport_options["displayLights"] = instance.data["displayLights"] viewport_options["imagePlane"] = instance.data.get("imagePlane", True) # Override transparency if requested. transparency = instance.data.get("transparency", 0) if transparency != 0: preset["viewport2_options"]["transparencyAlgorithm"] = transparency # Update preset with current panel setting # if override_viewport_options is turned off if not capture_preset["Viewport Options"]["override_viewport_options"]: panel_preset = capture.parse_view(preset["panel"]) panel_preset.pop("camera") preset.update(panel_preset) return preset @contextlib.contextmanager def viewport_default_options(panel, preset): """Context manager used by `render_capture_preset`. We need to explicitly enable some viewport changes so the viewport is refreshed ahead of playblasting. """ # TODO: Clarify in the docstring WHY we need to set it ahead of # playblasting. What issues does it solve? viewport_defaults = {} try: keys = [ "useDefaultMaterial", "wireframeOnShaded", "xray", "jointXray", "backfaceCulling", "textures" ] for key in keys: viewport_defaults[key] = cmds.modelEditor( panel, query=True, **{key: True} ) if preset["viewport_options"].get(key): cmds.modelEditor( panel, edit=True, **{key: True} ) yield finally: # Restoring viewport options. if viewport_defaults: cmds.modelEditor( panel, edit=True, **viewport_defaults ) @contextlib.contextmanager def material_loading_mode(mode="immediate"): """Set material loading mode during context""" original = cmds.displayPref(query=True, materialLoadingMode=True) cmds.displayPref(materialLoadingMode=mode) try: yield finally: cmds.displayPref(materialLoadingMode=original) def get_namespace(node): """Return namespace of given node""" node_name = node.rsplit("|", 1)[-1] if ":" in node_name: return node_name.rsplit(":", 1)[0] else: return "" def strip_namespace(node, namespace): """Strip given namespace from node path. The namespace will only be stripped from names if it starts with that namespace. If the namespace occurs within another namespace it's not removed. Examples: >>> strip_namespace("namespace:node", namespace="namespace:") "node" >>> strip_namespace("hello:world:node", namespace="hello:world") "node" >>> strip_namespace("hello:world:node", namespace="hello") "world:node" >>> strip_namespace("hello:world:node", namespace="world") "hello:world:node" >>> strip_namespace("ns:group|ns:node", namespace="ns") "group|node" Returns: str: Node name without given starting namespace. """ # Ensure namespace ends with `:` if not namespace.endswith(":"): namespace = "{}:".format(namespace) # The long path for a node can also have the namespace # in its parents so we need to remove it from each return "|".join( name[len(namespace):] if name.startswith(namespace) else name for name in node.split("|") ) def get_custom_namespace(custom_namespace): """Return unique namespace. The input namespace can contain a single group of '#' number tokens to indicate where the namespace's unique index should go. The amount of tokens defines the zero padding of the number, e.g ### turns into 001. Warning: Note that a namespace will always be prefixed with a _ if it starts with a digit Example: >>> get_custom_namespace("myspace_##_") # myspace_01_ >>> get_custom_namespace("##_myspace") # _01_myspace >>> get_custom_namespace("myspace##") # myspace01 """ split = re.split("([#]+)", custom_namespace, 1) if len(split) == 3: base, padding, suffix = split padding = "%0{}d".format(len(padding)) else: base = split[0] padding = "%02d" # default padding suffix = "" return unique_namespace( base, format=padding, prefix="_" if not base or base[0].isdigit() else "", suffix=suffix ) def unique_namespace(namespace, format="%02d", prefix="", suffix=""): """Return unique namespace Arguments: namespace (str): Name of namespace to consider format (str, optional): Formatting of the given iteration number suffix (str, optional): Only consider namespaces with this suffix. >>> unique_namespace("bar") # bar01 >>> unique_namespace(":hello") # :hello01 >>> unique_namespace("bar:", suffix="_NS") # bar01_NS: """ def current_namespace(): current = cmds.namespaceInfo(currentNamespace=True, absoluteName=True) # When inside a namespace Maya adds no trailing : if not current.endswith(":"): current += ":" return current # Always check against the absolute namespace root # There's no clash with :x if we're defining namespace :a:x ROOT = ":" if namespace.startswith(":") else current_namespace() # Strip trailing `:` tokens since we might want to add a suffix start = ":" if namespace.startswith(":") else "" end = ":" if namespace.endswith(":") else "" namespace = namespace.strip(":") if ":" in namespace: # Split off any nesting that we don't uniqify anyway. parents, namespace = namespace.rsplit(":", 1) start += parents + ":" ROOT += start def exists(n): # Check for clash with nodes and namespaces fullpath = ROOT + n return cmds.objExists(fullpath) or cmds.namespace(exists=fullpath) iteration = 1 while True: nr_namespace = namespace + format % iteration unique = prefix + nr_namespace + suffix if not exists(unique): return start + unique + end iteration += 1 def read(node): """Return user-defined attributes from `node`""" data = dict() for attr in cmds.listAttr(node, userDefined=True) or list(): try: value = cmds.getAttr(node + "." + attr, asString=True) except RuntimeError: # For Message type attribute or others that have connections, # take source node name as value. source = cmds.listConnections(node + "." + attr, source=True, destination=False) source = cmds.ls(source, long=True) or [None] value = source[0] except ValueError: # Some attributes cannot be read directly, # such as mesh and color attributes. These # are considered non-essential to this # particular publishing pipeline. value = None data[attr] = value return data def matrix_equals(a, b, tolerance=1e-10): """ Compares two matrices with an imperfection tolerance Args: a (list, tuple): the matrix to check b (list, tuple): the matrix to check against tolerance (float): the precision of the differences Returns: bool : True or False """ if not all(abs(x - y) < tolerance for x, y in zip(a, b)): return False return True def float_round(num, places=0, direction=ceil): return direction(num * (10**places)) / float(10**places) def pairwise(iterable): """s -> (s0,s1), (s2,s3), (s4, s5), ...""" from six.moves import zip a = iter(iterable) return zip(a, a) def collect_animation_defs(fps=False): """Get the basic animation attribute defintions for the publisher. Returns: OrderedDict """ # get scene values as defaults frame_start = cmds.playbackOptions(query=True, minTime=True) frame_end = cmds.playbackOptions(query=True, maxTime=True) frame_start_handle = cmds.playbackOptions( query=True, animationStartTime=True ) frame_end_handle = cmds.playbackOptions(query=True, animationEndTime=True) handle_start = frame_start - frame_start_handle handle_end = frame_end_handle - frame_end # build attributes defs = [ NumberDef("frameStart", label="Frame Start", default=frame_start, decimals=0), NumberDef("frameEnd", label="Frame End", default=frame_end, decimals=0), NumberDef("handleStart", label="Handle Start", default=handle_start, decimals=0), NumberDef("handleEnd", label="Handle End", default=handle_end, decimals=0), NumberDef("step", label="Step size", tooltip="A smaller step size means more samples and larger " "output files.\n" "A 1.0 step size is a single sample every frame.\n" "A 0.5 step size is two samples per frame.\n" "A 0.2 step size is five samples per frame.", default=1.0, decimals=3), ] if fps: current_fps = mel.eval('currentTimeUnitToFPS()') fps_def = NumberDef( "fps", label="FPS", default=current_fps, decimals=5 ) defs.append(fps_def) return defs def imprint(node, data): """Write `data` to `node` as userDefined attributes Arguments: node (str): Long name of node data (dict): Dictionary of key/value pairs Example: >>> from maya import cmds >>> def compute(): ... return 6 ... >>> cube, generator = cmds.polyCube() >>> imprint(cube, { ... "regularString": "myFamily", ... "computedValue": lambda: compute() ... }) ... >>> cmds.getAttr(cube + ".computedValue") 6 """ for key, value in data.items(): if callable(value): # Support values evaluated at imprint value = value() if isinstance(value, bool): add_type = {"attributeType": "bool"} set_type = {"keyable": False, "channelBox": True} elif isinstance(value, string_types): add_type = {"dataType": "string"} set_type = {"type": "string"} elif isinstance(value, int): add_type = {"attributeType": "long"} set_type = {"keyable": False, "channelBox": True} elif isinstance(value, float): add_type = {"attributeType": "double"} set_type = {"keyable": False, "channelBox": True} elif isinstance(value, (list, tuple)): add_type = {"attributeType": "enum", "enumName": ":".join(value)} set_type = {"keyable": False, "channelBox": True} value = 0 # enum default else: raise TypeError("Unsupported type: %r" % type(value)) cmds.addAttr(node, longName=key, **add_type) cmds.setAttr(node + "." + key, value, **set_type) def lsattr(attr, value=None): """Return nodes matching `key` and `value` Arguments: attr (str): Name of Maya attribute value (object, optional): Value of attribute. If none is provided, return all nodes with this attribute. Example: >> lsattr("id", "myId") ["myNode"] >> lsattr("id") ["myNode", "myOtherNode"] """ if value is None: return cmds.ls("*.%s" % attr, recursive=True, objectsOnly=True, long=True) return lsattrs({attr: value}) def lsattrs(attrs): """Return nodes with the given attribute(s). Arguments: attrs (dict): Name and value pairs of expected matches Example: >>> # Return nodes with an `age` of five. >>> lsattrs({"age": "five"}) >>> # Return nodes with both `age` and `color` of five and blue. >>> lsattrs({"age": "five", "color": "blue"}) Return: list: matching nodes. """ dep_fn = OpenMaya.MFnDependencyNode() dag_fn = OpenMaya.MFnDagNode() selection_list = OpenMaya.MSelectionList() first_attr = next(iter(attrs)) try: selection_list.add("*.{0}".format(first_attr), searchChildNamespaces=True) except RuntimeError as exc: if str(exc).endswith("Object does not exist"): return [] matches = set() for i in range(selection_list.length()): node = selection_list.getDependNode(i) if node.hasFn(OpenMaya.MFn.kDagNode): fn_node = dag_fn.setObject(node) full_path_names = [path.fullPathName() for path in fn_node.getAllPaths()] else: fn_node = dep_fn.setObject(node) full_path_names = [fn_node.name()] for attr in attrs: try: plug = fn_node.findPlug(attr, True) if plug.asString() != attrs[attr]: break except RuntimeError: break else: matches.update(full_path_names) return list(matches) @contextlib.contextmanager def attribute_values(attr_values): """Remaps node attributes to values during context. Arguments: attr_values (dict): Dictionary with (attr, value) """ original = [(attr, cmds.getAttr(attr)) for attr in attr_values] try: for attr, value in attr_values.items(): if isinstance(value, string_types): cmds.setAttr(attr, value, type="string") else: cmds.setAttr(attr, value) yield finally: for attr, value in original: if isinstance(value, string_types): cmds.setAttr(attr, value, type="string") elif value is None and cmds.getAttr(attr, type=True) == "string": # In some cases the maya.cmds.getAttr command returns None # for string attributes but this value cannot assigned. # Note: After setting it once to "" it will then return "" # instead of None. So this would only happen once. cmds.setAttr(attr, "", type="string") else: cmds.setAttr(attr, value) @contextlib.contextmanager def keytangent_default(in_tangent_type='auto', out_tangent_type='auto'): """Set the default keyTangent for new keys during this context""" original_itt = cmds.keyTangent(query=True, g=True, itt=True)[0] original_ott = cmds.keyTangent(query=True, g=True, ott=True)[0] cmds.keyTangent(g=True, itt=in_tangent_type) cmds.keyTangent(g=True, ott=out_tangent_type) try: yield finally: cmds.keyTangent(g=True, itt=original_itt) cmds.keyTangent(g=True, ott=original_ott) @contextlib.contextmanager def undo_chunk(): """Open a undo chunk during context.""" try: cmds.undoInfo(openChunk=True) yield finally: cmds.undoInfo(closeChunk=True) @contextlib.contextmanager def evaluation(mode="off"): """Set the evaluation manager during context. Arguments: mode (str): The mode to apply during context. "off": The standard DG evaluation (stable) "serial": A serial DG evaluation "parallel": The Maya 2016+ parallel evaluation """ original = cmds.evaluationManager(query=True, mode=1)[0] try: cmds.evaluationManager(mode=mode) yield finally: cmds.evaluationManager(mode=original) @contextlib.contextmanager def empty_sets(sets, force=False): """Remove all members of the sets during the context""" assert isinstance(sets, (list, tuple)) original = dict() original_connections = [] # Store original state for obj_set in sets: members = cmds.sets(obj_set, query=True) original[obj_set] = members try: for obj_set in sets: cmds.sets(clear=obj_set) if force: # Break all connections if force is enabled, this way we # prevent Maya from exporting any reference nodes which are # connected with placeHolder[x] attributes plug = "%s.dagSetMembers" % obj_set connections = cmds.listConnections(plug, source=True, destination=False, plugs=True, connections=True) or [] original_connections.extend(connections) for dest, src in pairwise(connections): cmds.disconnectAttr(src, dest) yield finally: for dest, src in pairwise(original_connections): cmds.connectAttr(src, dest) # Restore original members _iteritems = getattr(original, "iteritems", original.items) for origin_set, members in _iteritems(): cmds.sets(members, forceElement=origin_set) @contextlib.contextmanager def renderlayer(layer): """Set the renderlayer during the context Arguments: layer (str): Name of layer to switch to. """ original = cmds.editRenderLayerGlobals(query=True, currentRenderLayer=True) try: cmds.editRenderLayerGlobals(currentRenderLayer=layer) yield finally: cmds.editRenderLayerGlobals(currentRenderLayer=original) class delete_after(object): """Context Manager that will delete collected nodes after exit. This allows to ensure the nodes added to the context are deleted afterwards. This is useful if you want to ensure nodes are deleted even if an error is raised. Examples: with delete_after() as delete_bin: cube = maya.cmds.polyCube() delete_bin.extend(cube) # cube exists # cube deleted """ def __init__(self, nodes=None): self._nodes = list() if nodes: self.extend(nodes) def append(self, node): self._nodes.append(node) def extend(self, nodes): self._nodes.extend(nodes) def __iter__(self): return iter(self._nodes) def __enter__(self): return self def __exit__(self, type, value, traceback): if self._nodes: cmds.delete(self._nodes) def get_current_renderlayer(): return cmds.editRenderLayerGlobals(query=True, currentRenderLayer=True) def get_renderer(layer): with renderlayer(layer): return cmds.getAttr("defaultRenderGlobals.currentRenderer") @contextlib.contextmanager def no_undo(flush=False): """Disable the undo queue during the context Arguments: flush (bool): When True the undo queue will be emptied when returning from the context losing all undo history. Defaults to False. """ original = cmds.undoInfo(query=True, state=True) keyword = 'state' if flush else 'stateWithoutFlush' try: cmds.undoInfo(**{keyword: False}) yield finally: cmds.undoInfo(**{keyword: original}) def get_shader_assignments_from_shapes(shapes, components=True): """Return the shape assignment per related shading engines. Returns a dictionary where the keys are shadingGroups and the values are lists of assigned shapes or shape-components. Since `maya.cmds.sets` returns shader members on the shapes as components on the transform we correct that in this method too. For the 'shapes' this will return a dictionary like: { "shadingEngineX": ["nodeX", "nodeY"], "shadingEngineY": ["nodeA", "nodeB"] } Args: shapes (list): The shapes to collect the assignments for. components (bool): Whether to include the component assignments. Returns: dict: The {shadingEngine: shapes} relationships """ shapes = cmds.ls(shapes, long=True, shapes=True, objectsOnly=True) if not shapes: return {} # Collect shading engines and their shapes assignments = defaultdict(list) for shape in shapes: # Get unique shading groups for the shape shading_groups = cmds.listConnections(shape, source=False, destination=True, plugs=False, connections=False, type="shadingEngine") or [] shading_groups = list(set(shading_groups)) for shading_group in shading_groups: assignments[shading_group].append(shape) if components: # Note: Components returned from maya.cmds.sets are "listed" as if # being assigned to the transform like: pCube1.f[0] as opposed # to pCubeShape1.f[0] so we correct that here too. # Build a mapping from parent to shapes to include in lookup. transforms = {shape.rsplit("|", 1)[0]: shape for shape in shapes} lookup = set(shapes) | set(transforms.keys()) component_assignments = defaultdict(list) for shading_group in assignments.keys(): members = cmds.ls(cmds.sets(shading_group, query=True), long=True) for member in members: node = member.split(".", 1)[0] if node not in lookup: continue # Component if "." in member: # Fix transform to shape as shaders are assigned to shapes if node in transforms: shape = transforms[node] component = member.split(".", 1)[1] member = "{0}.{1}".format(shape, component) component_assignments[shading_group].append(member) assignments = component_assignments return dict(assignments) @contextlib.contextmanager def shader(nodes, shadingEngine="initialShadingGroup"): """Assign a shader to nodes during the context""" shapes = cmds.ls(nodes, dag=1, objectsOnly=1, shapes=1, long=1) original = get_shader_assignments_from_shapes(shapes) try: # Assign override shader if shapes: cmds.sets(shapes, edit=True, forceElement=shadingEngine) yield finally: # Assign original shaders for sg, members in original.items(): if members: cmds.sets(members, edit=True, forceElement=sg) @contextlib.contextmanager def displaySmoothness(nodes, divisionsU=0, divisionsV=0, pointsWire=4, pointsShaded=1, polygonObject=1): """Set the displaySmoothness during the context""" # Ensure only non-intermediate shapes nodes = cmds.ls(nodes, dag=1, shapes=1, long=1, noIntermediate=True) def parse(node): """Parse the current state of a node""" state = {} for key in ["divisionsU", "divisionsV", "pointsWire", "pointsShaded", "polygonObject"]: value = cmds.displaySmoothness(node, query=1, **{key: True}) if value is not None: state[key] = value[0] return state originals = dict((node, parse(node)) for node in nodes) try: # Apply current state cmds.displaySmoothness(nodes, divisionsU=divisionsU, divisionsV=divisionsV, pointsWire=pointsWire, pointsShaded=pointsShaded, polygonObject=polygonObject) yield finally: # Revert state _iteritems = getattr(originals, "iteritems", originals.items) for node, state in _iteritems(): if state: cmds.displaySmoothness(node, **state) @contextlib.contextmanager def no_display_layers(nodes): """Ensure nodes are not in a displayLayer during context. Arguments: nodes (list): The nodes to remove from any display layer. """ # Ensure long names nodes = cmds.ls(nodes, long=True) # Get the original state lookup = set(nodes) original = {} for layer in cmds.ls(type='displayLayer'): # Skip default layer if layer == "defaultLayer": continue members = cmds.editDisplayLayerMembers(layer, query=True, fullNames=True) if not members: continue members = set(members) included = lookup.intersection(members) if included: original[layer] = list(included) try: # Add all nodes to default layer cmds.editDisplayLayerMembers("defaultLayer", nodes, noRecurse=True) yield finally: # Restore original members _iteritems = getattr(original, "iteritems", original.items) for layer, members in _iteritems(): cmds.editDisplayLayerMembers(layer, members, noRecurse=True) @contextlib.contextmanager def namespaced(namespace, new=True, relative_names=None): """Work inside namespace during context Args: new (bool): When enabled this will rename the namespace to a unique namespace if the input namespace already exists. Yields: str: The namespace that is used during the context """ original = cmds.namespaceInfo(cur=True, absoluteName=True) original_relative_names = cmds.namespace(query=True, relativeNames=True) if new: namespace = unique_namespace(namespace) cmds.namespace(add=namespace) if relative_names is not None: cmds.namespace(relativeNames=relative_names) try: cmds.namespace(set=namespace) yield namespace finally: cmds.namespace(set=original) if relative_names is not None: cmds.namespace(relativeNames=original_relative_names) @contextlib.contextmanager def maintained_selection_api(): """Maintain selection using the Maya Python API. Warning: This is *not* added to the undo stack. """ original = OpenMaya.MGlobal.getActiveSelectionList() try: yield finally: OpenMaya.MGlobal.setActiveSelectionList(original) @contextlib.contextmanager def tool(context): """Set a tool context during the context manager. """ original = cmds.currentCtx() try: cmds.setToolTo(context) yield finally: cmds.setToolTo(original) def polyConstraint(components, *args, **kwargs): """Return the list of *components* with the constraints applied. A wrapper around Maya's `polySelectConstraint` to retrieve its results as a list without altering selections. For a list of possible constraints see `maya.cmds.polySelectConstraint` documentation. Arguments: components (list): List of components of polygon meshes Returns: list: The list of components filtered by the given constraints. """ kwargs.pop('mode', None) with no_undo(flush=False): # Reverting selection to the original selection using # `maya.cmds.select` can be slow in rare cases where previously # `maya.cmds.polySelectConstraint` had set constrain to "All and Next" # and the "Random" setting was activated. To work around this we # revert to the original selection using the Maya API. This is safe # since we're not generating any undo change anyway. with tool("selectSuperContext"): # Selection can be very slow when in a manipulator mode. # So we force the selection context which is fast. with maintained_selection_api(): # Apply constraint using mode=2 (current and next) so # it applies to the selection made before it; because just # a `maya.cmds.select()` call will not trigger the constraint. with reset_polySelectConstraint(): cmds.select(components, r=1, noExpand=True) cmds.polySelectConstraint(*args, mode=2, **kwargs) result = cmds.ls(selection=True) cmds.select(clear=True) return result @contextlib.contextmanager def reset_polySelectConstraint(reset=True): """Context during which the given polyConstraint settings are disabled. The original settings are restored after the context. """ original = cmds.polySelectConstraint(query=True, stateString=True) try: if reset: # Ensure command is available in mel # This can happen when running standalone if not mel.eval("exists resetPolySelectConstraint"): mel.eval("source polygonConstraint") # Reset all parameters mel.eval("resetPolySelectConstraint;") cmds.polySelectConstraint(disable=True) yield finally: mel.eval(original) def is_visible(node, displayLayer=True, intermediateObject=True, parentHidden=True, visibility=True): """Is `node` visible? Returns whether a node is hidden by one of the following methods: - The node exists (always checked) - The node must be a dagNode (always checked) - The node's visibility is off. - The node is set as intermediate Object. - The node is in a disabled displayLayer. - Whether any of its parent nodes is hidden. Roughly based on: http://ewertb.soundlinker.com/mel/mel.098.php Returns: bool: Whether the node is visible in the scene """ # Only existing objects can be visible if not cmds.objExists(node): return False # Only dagNodes can be visible if not cmds.objectType(node, isAType='dagNode'): return False if visibility: if not cmds.getAttr('{0}.visibility'.format(node)): return False if intermediateObject and cmds.objectType(node, isAType='shape'): if cmds.getAttr('{0}.intermediateObject'.format(node)): return False if displayLayer: # Display layers set overrideEnabled and overrideVisibility on members if cmds.attributeQuery('overrideEnabled', node=node, exists=True): override_enabled = cmds.getAttr('{}.overrideEnabled'.format(node)) override_visibility = cmds.getAttr('{}.overrideVisibility'.format( node)) if override_enabled and not override_visibility: return False if parentHidden: parents = cmds.listRelatives(node, parent=True, fullPath=True) if parents: parent = parents[0] if not is_visible(parent, displayLayer=displayLayer, intermediateObject=False, parentHidden=parentHidden, visibility=visibility): return False return True # region ID def get_id_required_nodes(referenced_nodes=False, nodes=None): """Filter out any node which are locked (reference) or readOnly Args: referenced_nodes (bool): set True to filter out reference nodes nodes (list, Optional): nodes to consider Returns: nodes (set): list of filtered nodes """ lookup = None if nodes is None: # Consider all nodes nodes = cmds.ls() else: # Build a lookup for the only allowed nodes in output based # on `nodes` input of the function (+ ensure long names) lookup = set(cmds.ls(nodes, long=True)) def _node_type_exists(node_type): try: cmds.nodeType(node_type, isTypeName=True) return True except RuntimeError: return False # `readOnly` flag is obsolete as of Maya 2016 therefore we explicitly # remove default nodes and reference nodes camera_shapes = ["frontShape", "sideShape", "topShape", "perspShape"] ignore = set() if not referenced_nodes: ignore |= set(cmds.ls(long=True, referencedNodes=True)) # list all defaultNodes to filter out from the rest ignore |= set(cmds.ls(long=True, defaultNodes=True)) ignore |= set(cmds.ls(camera_shapes, long=True)) # Remove Turtle from the result of `cmds.ls` if Turtle is loaded # TODO: This should be a less specific check for a single plug-in. if _node_type_exists("ilrBakeLayer"): ignore |= set(cmds.ls(type="ilrBakeLayer", long=True)) # Establish set of nodes types to include types = ["objectSet", "file", "mesh", "nurbsCurve", "nurbsSurface"] # Check if plugin nodes are available for Maya by checking if the plugin # is loaded if cmds.pluginInfo("pgYetiMaya", query=True, loaded=True): types.append("pgYetiMaya") # We *always* ignore intermediate shapes, so we filter them out directly nodes = cmds.ls(nodes, type=types, long=True, noIntermediate=True) # The items which need to pass the id to their parent # Add the collected transform to the nodes dag = cmds.ls(nodes, type="dagNode", long=True) # query only dag nodes transforms = cmds.listRelatives(dag, parent=True, fullPath=True) or [] nodes = set(nodes) nodes |= set(transforms) nodes -= ignore # Remove the ignored nodes if not nodes: return nodes # Ensure only nodes from the input `nodes` are returned when a # filter was applied on function call because we also iterated # to parents and alike if lookup is not None: nodes &= lookup # Avoid locked nodes nodes_list = list(nodes) locked = cmds.lockNode(nodes_list, query=True, lock=True) for node, lock in zip(nodes_list, locked): if lock: log.warning("Skipping locked node: %s" % node) nodes.remove(node) return nodes def get_id(node): """Get the `cbId` attribute of the given node. Args: node (str): the name of the node to retrieve the attribute from Returns: str """ if node is None: return sel = OpenMaya.MSelectionList() sel.add(node) api_node = sel.getDependNode(0) fn = OpenMaya.MFnDependencyNode(api_node) if not fn.hasAttribute("cbId"): return try: return fn.findPlug("cbId", False).asString() except RuntimeError: log.warning("Failed to retrieve cbId on %s", node) return def generate_ids(nodes, asset_id=None): """Returns new unique ids for the given nodes. Note: This does not assign the new ids, it only generates the values. To assign new ids using this method: >>> nodes = ["a", "b", "c"] >>> for node, id in generate_ids(nodes): >>> set_id(node, id) To also override any existing values (and assign regenerated ids): >>> nodes = ["a", "b", "c"] >>> for node, id in generate_ids(nodes): >>> set_id(node, id, overwrite=True) Args: nodes (list): List of nodes. asset_id (str or bson.ObjectId): The database id for the *asset* to generate for. When None provided the current asset in the active session is used. Returns: list: A list of (node, id) tuples. """ if asset_id is None: # Get the asset ID from the database for the asset of current context project_name = get_current_project_name() asset_name = get_current_asset_name() asset_doc = get_asset_by_name(project_name, asset_name, fields=["_id"]) assert asset_doc, "No current asset found in Session" asset_id = asset_doc['_id'] node_ids = [] for node in nodes: _, uid = str(uuid.uuid4()).rsplit("-", 1) unique_id = "{}:{}".format(asset_id, uid) node_ids.append((node, unique_id)) return node_ids def set_id(node, unique_id, overwrite=False): """Add cbId to `node` unless one already exists. Args: node (str): the node to add the "cbId" on unique_id (str): The unique node id to assign. This should be generated by `generate_ids`. overwrite (bool, optional): When True overrides the current value even if `node` already has an id. Defaults to False. Returns: None """ exists = cmds.attributeQuery("cbId", node=node, exists=True) # Add the attribute if it does not exist yet if not exists: cmds.addAttr(node, longName="cbId", dataType="string") # Set the value if not exists or overwrite: attr = "{0}.cbId".format(node) cmds.setAttr(attr, unique_id, type="string") def get_attribute(plug, asString=False, expandEnvironmentVariables=False, **kwargs): """Maya getAttr with some fixes based on `pymel.core.general.getAttr()`. Like Pymel getAttr this applies some changes to `maya.cmds.getAttr` - maya pointlessly returned vector results as a tuple wrapped in a list (ex. '[(1,2,3)]'). This command unpacks the vector for you. - when getting a multi-attr, maya would raise an error, but this will return a list of values for the multi-attr - added support for getting message attributes by returning the connections instead Note that the asString + expandEnvironmentVariables argument naming convention matches the `maya.cmds.getAttr` arguments so that it can act as a direct replacement for it. Args: plug (str): Node's attribute plug as `node.attribute` asString (bool): Return string value for enum attributes instead of the index. Note that the return value can be dependent on the UI language Maya is running in. expandEnvironmentVariables (bool): Expand any environment variable and (tilde characters on UNIX) found in string attributes which are returned. Kwargs: Supports the keyword arguments of `maya.cmds.getAttr` Returns: object: The value of the maya attribute. """ attr_type = cmds.getAttr(plug, type=True) if asString: kwargs["asString"] = True if expandEnvironmentVariables: kwargs["expandEnvironmentVariables"] = True try: res = cmds.getAttr(plug, **kwargs) except RuntimeError: if attr_type == "message": return cmds.listConnections(plug) node, attr = plug.split(".", 1) children = cmds.attributeQuery(attr, node=node, listChildren=True) if children: return [ get_attribute("{}.{}".format(node, child)) for child in children ] raise # Convert vector result wrapped in tuple if isinstance(res, list) and len(res): if isinstance(res[0], tuple) and len(res): if attr_type in {'pointArray', 'vectorArray'}: return res return res[0] return res def set_attribute(attribute, value, node): """Adjust attributes based on the value from the attribute data If an attribute does not exists on the target it will be added with the dataType being controlled by the value type. Args: attribute (str): name of the attribute to change value: the value to change to attribute to node (str): name of the node Returns: None """ value_type = type(value).__name__ kwargs = ATTRIBUTE_DICT[value_type] if not cmds.attributeQuery(attribute, node=node, exists=True): log.debug("Creating attribute '{}' on " "'{}'".format(attribute, node)) cmds.addAttr(node, longName=attribute, **kwargs) node_attr = "{}.{}".format(node, attribute) enum_type = cmds.attributeQuery(attribute, node=node, enum=True) if enum_type and value_type == "str": enum_string_values = cmds.attributeQuery( attribute, node=node, listEnum=True )[0].split(":") cmds.setAttr( "{}.{}".format(node, attribute), enum_string_values.index(value) ) elif "dataType" in kwargs: attr_type = kwargs["dataType"] cmds.setAttr(node_attr, value, type=attr_type) else: cmds.setAttr(node_attr, value) def apply_attributes(attributes, nodes_by_id): """Alter the attributes to match the state when publishing Apply attribute settings from the publish to the node in the scene based on the UUID which is stored in the cbId attribute. Args: attributes (list): list of dictionaries nodes_by_id (dict): collection of nodes based on UUID {uuid: [node, node]} """ for attr_data in attributes: nodes = nodes_by_id[attr_data["uuid"]] attr_value = attr_data["attributes"] for node in nodes: for attr, value in attr_value.items(): set_attribute(attr, value, node) def get_container_members(container): """Returns the members of a container. This includes the nodes from any loaded references in the container. """ if isinstance(container, dict): # Assume it's a container dictionary container = container["objectName"] members = cmds.sets(container, query=True) or [] members = cmds.ls(members, long=True, objectsOnly=True) or [] all_members = set(members) # Include any referenced nodes from any reference in the container # This is required since we've removed adding ALL nodes of a reference # into the container set and only add the reference node now. for ref in cmds.ls(members, exactType="reference", objectsOnly=True): # Ignore any `:sharedReferenceNode` if ref.rsplit(":", 1)[-1].startswith("sharedReferenceNode"): continue # Ignore _UNKNOWN_REF_NODE_ (PLN-160) if ref.rsplit(":", 1)[-1].startswith("_UNKNOWN_REF_NODE_"): continue reference_members = cmds.referenceQuery(ref, nodes=True, dagPath=True) reference_members = cmds.ls(reference_members, long=True, objectsOnly=True) all_members.update(reference_members) return list(all_members) # region LOOKDEV def list_looks(project_name, asset_id): """Return all look subsets for the given asset This assumes all look subsets start with "look*" in their names. """ # # get all subsets with look leading in # the name associated with the asset # TODO this should probably look for family 'look' instead of checking # subset name that can not start with family subset_docs = get_subsets(project_name, asset_ids=[asset_id]) return [ subset_doc for subset_doc in subset_docs if subset_doc["name"].startswith("look") ] def assign_look_by_version(nodes, version_id): """Assign nodes a specific published look version by id. This assumes the nodes correspond with the asset. Args: nodes(list): nodes to assign look to version_id (bson.ObjectId): database id of the version Returns: None """ project_name = get_current_project_name() # Get representations of shader file and relationships look_representation = get_representation_by_name( project_name, "ma", version_id ) json_representation = get_representation_by_name( project_name, "json", version_id ) # See if representation is already loaded, if so reuse it. host = registered_host() representation_id = str(look_representation['_id']) for container in host.ls(): if (container['loader'] == "LookLoader" and container['representation'] == representation_id): log.info("Reusing loaded look ..") container_node = container['objectName'] break else: log.info("Using look for the first time ..") # Load file _loaders = discover_loader_plugins() loaders = loaders_from_representation(_loaders, representation_id) Loader = next((i for i in loaders if i.__name__ == "LookLoader"), None) if Loader is None: raise RuntimeError("Could not find LookLoader, this is a bug") # Reference the look file with maintained_selection(): container_node = load_container(Loader, look_representation) # Get container members shader_nodes = get_container_members(container_node) # Load relationships shader_relation = get_representation_path(json_representation) with open(shader_relation, "r") as f: relationships = json.load(f) # Assign relationships apply_shaders(relationships, shader_nodes, nodes) def assign_look(nodes, subset="lookDefault"): """Assigns a look to a node. Optimizes the nodes by grouping by asset id and finding related subset by name. Args: nodes (list): all nodes to assign the look to subset (str): name of the subset to find """ # Group all nodes per asset id grouped = defaultdict(list) for node in nodes: pype_id = get_id(node) if not pype_id: continue parts = pype_id.split(":", 1) grouped[parts[0]].append(node) project_name = get_current_project_name() subset_docs = get_subsets( project_name, subset_names=[subset], asset_ids=grouped.keys() ) subset_docs_by_asset_id = { str(subset_doc["parent"]): subset_doc for subset_doc in subset_docs } subset_ids = { subset_doc["_id"] for subset_doc in subset_docs_by_asset_id.values() } last_version_docs = get_last_versions( project_name, subset_ids=subset_ids, fields=["_id", "name", "data.families"] ) last_version_docs_by_subset_id = { last_version_doc["parent"]: last_version_doc for last_version_doc in last_version_docs } for asset_id, asset_nodes in grouped.items(): # create objectId for database subset_doc = subset_docs_by_asset_id.get(asset_id) if not subset_doc: log.warning("No subset '{}' found for {}".format(subset, asset_id)) continue last_version = last_version_docs_by_subset_id.get(subset_doc["_id"]) if not last_version: log.warning(( "Not found last version for subset '{}' on asset with id {}" ).format(subset, asset_id)) continue families = last_version.get("data", {}).get("families") or [] if "look" not in families: log.warning(( "Last version for subset '{}' on asset with id {}" " does not have look family" ).format(subset, asset_id)) continue log.debug("Assigning look '{}' ".format( subset, last_version["name"])) assign_look_by_version(asset_nodes, last_version["_id"]) def apply_shaders(relationships, shadernodes, nodes): """Link shadingEngine to the right nodes based on relationship data Relationship data is constructed of a collection of `sets` and `attributes` `sets` corresponds with the shaderEngines found in the lookdev. Each set has the keys `name`, `members` and `uuid`, the `members` hold a collection of node information `name` and `uuid`. Args: relationships (dict): relationship data shadernodes (list): list of nodes of the shading objectSets (includes VRayObjectProperties and shadingEngines) nodes (list): list of nodes to apply shader to Returns: None """ attributes = relationships.get("attributes", []) shader_data = relationships.get("relationships", {}) shading_engines = cmds.ls(shadernodes, type="objectSet", long=True) assert shading_engines, "Error in retrieving objectSets from reference" # region compute lookup nodes_by_id = defaultdict(list) for node in nodes: nodes_by_id[get_id(node)].append(node) shading_engines_by_id = defaultdict(list) for shad in shading_engines: shading_engines_by_id[get_id(shad)].append(shad) # endregion # region assign shading engines and other sets for data in shader_data.values(): # collect all unique IDs of the set members shader_uuid = data["uuid"] member_uuids = [member["uuid"] for member in data["members"]] filtered_nodes = list() for m_uuid in member_uuids: filtered_nodes.extend(nodes_by_id[m_uuid]) id_shading_engines = shading_engines_by_id[shader_uuid] if not id_shading_engines: log.error("No shader found with cbId " "'{}'".format(shader_uuid)) continue elif len(id_shading_engines) > 1: log.error("Skipping shader assignment. " "More than one shader found with cbId " "'{}'. (found: {})".format(shader_uuid, id_shading_engines)) continue if not filtered_nodes: log.warning("No nodes found for shading engine " "'{0}'".format(id_shading_engines[0])) continue try: cmds.sets(filtered_nodes, forceElement=id_shading_engines[0]) except RuntimeError as rte: log.error("Error during shader assignment: {}".format(rte)) # endregion apply_attributes(attributes, nodes_by_id) # endregion LOOKDEV def get_isolate_view_sets(): """Return isolate view sets of all modelPanels. Returns: list: all sets related to isolate view """ view_sets = set() for panel in cmds.getPanel(type="modelPanel") or []: view_set = cmds.modelEditor(panel, query=True, viewObjects=True) if view_set: view_sets.add(view_set) return view_sets def get_related_sets(node): """Return objectSets that are relationships for a look for `node`. Filters out based on: - id attribute is NOT `pyblish.avalon.container` - shapes and deformer shapes (alembic creates meshShapeDeformed) - set name ends with any from a predefined list - set in not in viewport set (isolate selected for example) Args: node (str): name of the current node to check Returns: list: The related sets """ # Ignore specific suffices ignore_suffices = ["out_SET", "controls_SET", "_INST", "_CON"] # Default nodes to ignore defaults = {"defaultLightSet", "defaultObjectSet"} # Ids to ignore ignored = {"pyblish.avalon.instance", "pyblish.avalon.container"} view_sets = get_isolate_view_sets() sets = cmds.listSets(object=node, extendToShape=False) if not sets: return [] # Fix 'no object matches name' errors on nodes returned by listSets. # In rare cases it can happen that a node is added to an internal maya # set inaccessible by maya commands, for example check some nodes # returned by `cmds.listSets(allSets=True)` sets = cmds.ls(sets) # Ignore `avalon.container` sets = [s for s in sets if not cmds.attributeQuery("id", node=s, exists=True) or not cmds.getAttr("%s.id" % s) in ignored] # Exclude deformer sets (`type=2` for `maya.cmds.listSets`) deformer_sets = cmds.listSets(object=node, extendToShape=False, type=2) or [] deformer_sets = set(deformer_sets) # optimize lookup sets = [s for s in sets if s not in deformer_sets] # Ignore when the set has a specific suffix sets = [s for s in sets if not any(s.endswith(x) for x in ignore_suffices)] # Ignore viewport filter view sets (from isolate select and # viewports) sets = [s for s in sets if s not in view_sets] sets = [s for s in sets if s not in defaults] return sets def get_container_transforms(container, members=None, root=False): """Retrieve the root node of the container content When a container is created through a Loader the content of the file will be grouped under a transform. The name of the root transform is stored in the container information Args: container (dict): the container members (list): optional and convenience argument root (bool): return highest node in hierarchy if True Returns: root (list / str): """ if not members: members = get_container_members(container) results = cmds.ls(members, type="transform", long=True) if root: root = get_highest_in_hierarchy(results) if root: results = root[0] return results def get_highest_in_hierarchy(nodes): """Return highest nodes in the hierarchy that are in the `nodes` list. The "highest in hierarchy" are the nodes closest to world: top-most level. Args: nodes (list): The nodes in which find the highest in hierarchies. Returns: list: The highest nodes from the input nodes. """ # Ensure we use long names nodes = cmds.ls(nodes, long=True) lookup = set(nodes) highest = [] for node in nodes: # If no parents are within the nodes input list # then this is a highest node if not any(n in lookup for n in iter_parents(node)): highest.append(node) return highest def iter_parents(node): """Iter parents of node from its long name. Note: The `node` *must* be the long node name. Args: node (str): Node long name. Yields: str: All parent node names (long names) """ while True: split = node.rsplit("|", 1) if len(split) == 1 or not split[0]: return node = split[0] yield node def remove_other_uv_sets(mesh): """Remove all other UV sets than the current UV set. Keep only current UV set and ensure it's the renamed to default 'map1'. """ uvSets = cmds.polyUVSet(mesh, query=True, allUVSets=True) current = cmds.polyUVSet(mesh, query=True, currentUVSet=True)[0] # Copy over to map1 if current != 'map1': cmds.polyUVSet(mesh, uvSet=current, newUVSet='map1', copy=True) cmds.polyUVSet(mesh, currentUVSet=True, uvSet='map1') current = 'map1' # Delete all non-current UV sets deleteUVSets = [uvSet for uvSet in uvSets if uvSet != current] uvSet = None # Maya Bug (tested in 2015/2016): # In some cases the API's MFnMesh will report less UV sets than # maya.cmds.polyUVSet. This seems to happen when the deletion of UV sets # has not triggered a cleanup of the UVSet array attribute on the mesh # node. It will still have extra entries in the attribute, though it will # not show up in API or UI. Nevertheless it does show up in # maya.cmds.polyUVSet. To ensure we clean up the array we'll force delete # the extra remaining 'indices' that we don't want. # TODO: Implement a better fix # The best way to fix would be to get the UVSet indices from api with # MFnMesh (to ensure we keep correct ones) and then only force delete the # other entries in the array attribute on the node. But for now we're # deleting all entries except first one. Note that the first entry could # never be removed (the default 'map1' always exists and is supposed to # be undeletable.) try: for uvSet in deleteUVSets: cmds.polyUVSet(mesh, delete=True, uvSet=uvSet) except RuntimeError as exc: log.warning('Error uvSet: %s - %s', uvSet, exc) indices = cmds.getAttr('{0}.uvSet'.format(mesh), multiIndices=True) if not indices: log.warning("No uv set found indices for: %s", mesh) return # Delete from end to avoid shifting indices # and remove the indices in the attribute indices = reversed(indices[1:]) for i in indices: attr = '{0}.uvSet[{1}]'.format(mesh, i) cmds.removeMultiInstance(attr, b=True) def get_node_parent(node): """Return full path name for parent of node""" parents = cmds.listRelatives(node, parent=True, fullPath=True) return parents[0] if parents else None def get_id_from_sibling(node, history_only=True): """Return first node id in the history chain that matches this node. The nodes in history must be of the exact same node type and must be parented under the same parent. Optionally, if no matching node is found from the history, all the siblings of the node that are of the same type are checked. Additionally to having the same parent, the sibling must be marked as 'intermediate object'. Args: node (str): node to retrieve the history from history_only (bool): if True and if nothing found in history, look for an 'intermediate object' in all the node's siblings of same type Returns: str or None: The id from the sibling node or None when no id found on any valid nodes in the history or siblings. """ node = cmds.ls(node, long=True)[0] # Find all similar nodes in history history = cmds.listHistory(node) node_type = cmds.nodeType(node) similar_nodes = cmds.ls(history, exactType=node_type, long=True) # Exclude itself similar_nodes = [x for x in similar_nodes if x != node] # The node *must be* under the same parent parent = get_node_parent(node) similar_nodes = [i for i in similar_nodes if get_node_parent(i) == parent] # Check all of the remaining similar nodes and take the first one # with an id and assume it's the original. for similar_node in similar_nodes: _id = get_id(similar_node) if _id: return _id if not history_only: # Get siblings of same type similar_nodes = cmds.listRelatives(parent, type=node_type, fullPath=True) similar_nodes = cmds.ls(similar_nodes, exactType=node_type, long=True) # Exclude itself similar_nodes = [x for x in similar_nodes if x != node] # Get all unique ids from siblings in order since # we consistently take the first one found sibling_ids = OrderedDict() for similar_node in similar_nodes: # Check if "intermediate object" if not cmds.getAttr(similar_node + ".intermediateObject"): continue _id = get_id(similar_node) if not _id: continue if _id in sibling_ids: sibling_ids[_id].append(similar_node) else: sibling_ids[_id] = [similar_node] if sibling_ids: first_id, found_nodes = next(iter(sibling_ids.items())) # Log a warning if we've found multiple unique ids if len(sibling_ids) > 1: log.warning(("Found more than 1 intermediate shape with" " unique id for '{}'. Using id of first" " found: '{}'".format(node, found_nodes[0]))) return first_id def set_scene_fps(fps, update=True): """Set FPS from project configuration Args: fps (int, float): desired FPS update(bool): toggle update animation, default is True Returns: None """ fps_mapping = { '15': 'game', '24': 'film', '25': 'pal', '30': 'ntsc', '48': 'show', '50': 'palf', '60': 'ntscf', '23.976023976023978': '23.976fps', '29.97002997002997': '29.97fps', '47.952047952047955': '47.952fps', '59.94005994005994': '59.94fps', '44100': '44100fps', '48000': '48000fps' } unit = fps_mapping.get(str(convert_to_maya_fps(fps)), None) if unit is None: raise ValueError("Unsupported FPS value: `%s`" % fps) # Get time slider current state start_frame = cmds.playbackOptions(query=True, minTime=True) end_frame = cmds.playbackOptions(query=True, maxTime=True) # Get animation data animation_start = cmds.playbackOptions(query=True, animationStartTime=True) animation_end = cmds.playbackOptions(query=True, animationEndTime=True) current_frame = cmds.currentTime(query=True) log.info("Setting scene FPS to: '{}'".format(unit)) cmds.currentUnit(time=unit, updateAnimation=update) # Set time slider data back to previous state cmds.playbackOptions(edit=True, minTime=start_frame) cmds.playbackOptions(edit=True, maxTime=end_frame) # Set animation data cmds.playbackOptions(edit=True, animationStartTime=animation_start) cmds.playbackOptions(edit=True, animationEndTime=animation_end) cmds.currentTime(current_frame, edit=True, update=True) # Force file stated to 'modified' cmds.file(modified=True) def set_scene_resolution(width, height, pixelAspect): """Set the render resolution Args: width(int): value of the width height(int): value of the height Returns: None """ control_node = "defaultResolution" current_renderer = cmds.getAttr("defaultRenderGlobals.currentRenderer") aspect_ratio_attr = "deviceAspectRatio" # Give VRay a helping hand as it is slightly different from the rest if current_renderer == "vray": aspect_ratio_attr = "aspectRatio" vray_node = "vraySettings" if cmds.objExists(vray_node): control_node = vray_node else: log.error("Can't set VRay resolution because there is no node " "named: `%s`" % vray_node) log.info("Setting scene resolution to: %s x %s" % (width, height)) cmds.setAttr("%s.width" % control_node, width) cmds.setAttr("%s.height" % control_node, height) deviceAspectRatio = ((float(width) / float(height)) * float(pixelAspect)) cmds.setAttr( "{}.{}".format(control_node, aspect_ratio_attr), deviceAspectRatio) cmds.setAttr("%s.pixelAspect" % control_node, pixelAspect) def get_fps_for_current_context(): """Get fps that should be set for current context. Todos: - Skip project value. - Merge logic with 'get_frame_range' and 'reset_scene_resolution' -> all the values in the functions can be collected at one place as they have same requirements. Returns: Union[int, float]: FPS value. """ project_name = get_current_project_name() asset_name = get_current_asset_name() asset_doc = get_asset_by_name( project_name, asset_name, fields=["data.fps"] ) or {} fps = asset_doc.get("data", {}).get("fps") if not fps: project_doc = get_project(project_name, fields=["data.fps"]) or {} fps = project_doc.get("data", {}).get("fps") if not fps: fps = 25 return convert_to_maya_fps(fps) def get_frame_range(include_animation_range=False): """Get the current assets frame range and handles. Args: include_animation_range (bool, optional): Whether to include `animationStart` and `animationEnd` keys to define the outer range of the timeline. It is excluded by default. Returns: dict: Asset's expected frame range values. """ # Set frame start/end project_name = get_current_project_name() asset_name = get_current_asset_name() asset = get_asset_by_name(project_name, asset_name) frame_start = asset["data"].get("frameStart") frame_end = asset["data"].get("frameEnd") if frame_start is None or frame_end is None: cmds.warning("No edit information found for %s" % asset_name) return handle_start = asset["data"].get("handleStart") or 0 handle_end = asset["data"].get("handleEnd") or 0 frame_range = { "frameStart": frame_start, "frameEnd": frame_end, "handleStart": handle_start, "handleEnd": handle_end } if include_animation_range: # The animation range values are only included to define whether # the Maya time slider should include the handles or not. # Some usages of this function use the full dictionary to define # instance attributes for which we want to exclude the animation # keys. That is why these are excluded by default. task_name = get_current_task_name() settings = get_project_settings(project_name) include_handles_settings = settings["maya"]["include_handles"] current_task = asset.get("data").get("tasks").get(task_name) animation_start = frame_start animation_end = frame_end include_handles = include_handles_settings["include_handles_default"] for item in include_handles_settings["per_task_type"]: if current_task["type"] in item["task_type"]: include_handles = item["include_handles"] break if include_handles: animation_start -= int(handle_start) animation_end += int(handle_end) frame_range["animationStart"] = animation_start frame_range["animationEnd"] = animation_end return frame_range def reset_frame_range(playback=True, render=True, fps=True): """Set frame range to current asset Args: playback (bool, Optional): Whether to set the maya timeline playback frame range. Defaults to True. render (bool, Optional): Whether to set the maya render frame range. Defaults to True. fps (bool, Optional): Whether to set scene FPS. Defaults to True. """ if fps: set_scene_fps(get_fps_for_current_context()) frame_range = get_frame_range(include_animation_range=True) if not frame_range: # No frame range data found for asset return frame_start = frame_range["frameStart"] frame_end = frame_range["frameEnd"] animation_start = frame_range["animationStart"] animation_end = frame_range["animationEnd"] if playback: cmds.playbackOptions( minTime=frame_start, maxTime=frame_end, animationStartTime=animation_start, animationEndTime=animation_end ) cmds.currentTime(frame_start) if render: cmds.setAttr("defaultRenderGlobals.startFrame", animation_start) cmds.setAttr("defaultRenderGlobals.endFrame", animation_end) def reset_scene_resolution(): """Apply the scene resolution from the project definition scene resolution can be overwritten by an asset if the asset.data contains any information regarding scene resolution . Returns: None """ project_name = get_current_project_name() project_doc = get_project(project_name) project_data = project_doc["data"] asset_data = get_current_project_asset()["data"] # Set project resolution width_key = "resolutionWidth" height_key = "resolutionHeight" pixelAspect_key = "pixelAspect" width = asset_data.get(width_key, project_data.get(width_key, 1920)) height = asset_data.get(height_key, project_data.get(height_key, 1080)) pixelAspect = asset_data.get(pixelAspect_key, project_data.get(pixelAspect_key, 1)) set_scene_resolution(width, height, pixelAspect) def set_context_settings(): """Apply the project settings from the project definition Settings can be overwritten by an asset if the asset.data contains any information regarding those settings. Examples of settings: fps resolution renderer Returns: None """ # Set project fps set_scene_fps(get_fps_for_current_context()) reset_scene_resolution() # Set frame range. reset_frame_range() # Set colorspace set_colorspace() # Valid FPS def validate_fps(): """Validate current scene FPS and show pop-up when it is incorrect Returns: bool """ expected_fps = get_fps_for_current_context() current_fps = mel.eval('currentTimeUnitToFPS()') fps_match = current_fps == expected_fps if not fps_match and not IS_HEADLESS: from openpype.widgets import popup parent = get_main_window() dialog = popup.PopupUpdateKeys(parent=parent) dialog.setModal(True) dialog.setWindowTitle("Maya scene does not match project FPS") dialog.setMessage( "Scene {} FPS does not match project {} FPS".format( current_fps, expected_fps ) ) dialog.setButtonText("Fix") # Set new text for button (add optional argument for the popup?) toggle = dialog.widgets["toggle"] update = toggle.isChecked() dialog.on_clicked_state.connect( lambda: set_scene_fps(expected_fps, update) ) dialog.show() return False return fps_match def bake(nodes, frame_range=None, step=1.0, simulation=True, preserve_outside_keys=False, disable_implicit_control=True, shape=True): """Bake the given nodes over the time range. This will bake all attributes of the node, including custom attributes. Args: nodes (list): Names of transform nodes, eg. camera, light. frame_range (list): frame range with start and end frame. or if None then takes timeSliderRange simulation (bool): Whether to perform a full simulation of the attributes over time. preserve_outside_keys (bool): Keep keys that are outside of the baked range. disable_implicit_control (bool): When True will disable any constraints to the object. shape (bool): When True also bake attributes on the children shapes. step (float): The step size to sample by. Returns: None """ # Parse inputs if not nodes: return assert isinstance(nodes, (list, tuple)), "Nodes must be a list or tuple" # If frame range is None fall back to time slider playback time range if frame_range is None: frame_range = [cmds.playbackOptions(query=True, minTime=True), cmds.playbackOptions(query=True, maxTime=True)] # If frame range is single frame bake one frame more, # otherwise maya.cmds.bakeResults gets confused if frame_range[1] == frame_range[0]: frame_range[1] += 1 # Bake it with keytangent_default(in_tangent_type='auto', out_tangent_type='auto'): cmds.bakeResults(nodes, simulation=simulation, preserveOutsideKeys=preserve_outside_keys, disableImplicitControl=disable_implicit_control, shape=shape, sampleBy=step, time=(frame_range[0], frame_range[1])) def bake_to_world_space(nodes, frame_range=None, simulation=True, preserve_outside_keys=False, disable_implicit_control=True, shape=True, step=1.0): """Bake the nodes to world space transformation (incl. other attributes) Bakes the transforms to world space (while maintaining all its animated attributes and settings) by duplicating the node. Then parents it to world and constrains to the original. Other attributes are also baked by connecting all attributes directly. Baking is then done using Maya's bakeResults command. See `bake` for the argument documentation. Returns: list: The newly created and baked node names. """ @contextlib.contextmanager def _unlock_attr(attr): """Unlock attribute during context if it is locked""" if not cmds.getAttr(attr, lock=True): # If not locked, do nothing yield return try: cmds.setAttr(attr, lock=False) yield finally: cmds.setAttr(attr, lock=True) def _get_attrs(node): """Workaround for buggy shape attribute listing with listAttr This will only return keyable settable attributes that have an incoming connections (those that have a reason to be baked). Technically this *may* fail to return attributes driven by complex expressions for which maya makes no connections, e.g. doing actual `setAttr` calls in expressions. Arguments: node (str): The node to list attributes for. Returns: list: Keyable attributes with incoming connections. The attribute may be locked. """ attrs = cmds.listAttr(node, write=True, scalar=True, settable=True, connectable=True, keyable=True, shortNames=True) or [] valid_attrs = [] for attr in attrs: node_attr = '{0}.{1}'.format(node, attr) # Sometimes Maya returns 'non-existent' attributes for shapes # so we filter those out if not cmds.attributeQuery(attr, node=node, exists=True): continue # We only need those that have a connection, just to be safe # that it's actually keyable/connectable anyway. if cmds.connectionInfo(node_attr, isDestination=True): valid_attrs.append(attr) return valid_attrs transform_attrs = {"t", "r", "s", "tx", "ty", "tz", "rx", "ry", "rz", "sx", "sy", "sz"} world_space_nodes = [] with ExitStack() as stack: delete_bin = stack.enter_context(delete_after()) # Create the duplicate nodes that are in world-space connected to # the originals for node in nodes: # Duplicate the node short_name = node.rsplit("|", 1)[-1] new_name = "{0}_baked".format(short_name) new_node = cmds.duplicate(node, name=new_name, renameChildren=True)[0] # noqa # Parent new node to world if cmds.listRelatives(new_node, parent=True): new_node = cmds.parent(new_node, world=True)[0] # Temporarily unlock and passthrough connect all attributes # so we can bake them over time # Skip transform attributes because we will constrain them later attrs = set(_get_attrs(node)) - transform_attrs for attr in attrs: orig_node_attr = "{}.{}".format(node, attr) new_node_attr = "{}.{}".format(new_node, attr) # unlock during context to avoid connection errors stack.enter_context(_unlock_attr(new_node_attr)) cmds.connectAttr(orig_node_attr, new_node_attr, force=True) # If shapes are also baked then also temporarily unlock and # passthrough connect all shape attributes for baking if shape: children_shapes = cmds.listRelatives(new_node, children=True, fullPath=True, shapes=True) if children_shapes: orig_children_shapes = cmds.listRelatives(node, children=True, fullPath=True, shapes=True) for orig_shape, new_shape in zip(orig_children_shapes, children_shapes): attrs = _get_attrs(orig_shape) for attr in attrs: orig_node_attr = "{}.{}".format(orig_shape, attr) new_node_attr = "{}.{}".format(new_shape, attr) # unlock during context to avoid connection errors stack.enter_context(_unlock_attr(new_node_attr)) cmds.connectAttr(orig_node_attr, new_node_attr, force=True) # Constraint transforms for attr in transform_attrs: transform_attr = "{}.{}".format(new_node, attr) stack.enter_context(_unlock_attr(transform_attr)) delete_bin.extend(cmds.parentConstraint(node, new_node, mo=False)) delete_bin.extend(cmds.scaleConstraint(node, new_node, mo=False)) world_space_nodes.append(new_node) bake(world_space_nodes, frame_range=frame_range, step=step, simulation=simulation, preserve_outside_keys=preserve_outside_keys, disable_implicit_control=disable_implicit_control, shape=shape) return world_space_nodes def load_capture_preset(data): """Convert OpenPype Extract Playblast settings to `capture` arguments Input data is the settings from: `project_settings/maya/publish/ExtractPlayblast/capture_preset` Args: data (dict): Capture preset settings from OpenPype settings Returns: dict: `capture.capture` compatible keyword arguments """ options = dict() viewport_options = dict() viewport2_options = dict() camera_options = dict() # Straight key-value match from settings to capture arguments options.update(data["Codec"]) options.update(data["Generic"]) options.update(data["Resolution"]) camera_options.update(data['Camera Options']) viewport_options.update(data["Renderer"]) # DISPLAY OPTIONS disp_options = {} for key, value in data['Display Options'].items(): if key.startswith('background'): # Convert background, backgroundTop, backgroundBottom colors if len(value) == 4: # Ignore alpha + convert RGB to float value = [ float(value[0]) / 255, float(value[1]) / 255, float(value[2]) / 255 ] disp_options[key] = value elif key == "displayGradient": disp_options[key] = value options['display_options'] = disp_options # Viewport Options has a mixture of Viewport2 Options and Viewport Options # to pass along to capture. So we'll need to differentiate between the two VIEWPORT2_OPTIONS = { "textureMaxResolution", "renderDepthOfField", "ssaoEnable", "ssaoSamples", "ssaoAmount", "ssaoRadius", "ssaoFilterRadius", "hwFogStart", "hwFogEnd", "hwFogAlpha", "hwFogFalloff", "hwFogColorR", "hwFogColorG", "hwFogColorB", "hwFogDensity", "motionBlurEnable", "motionBlurSampleCount", "motionBlurShutterOpenFraction", "lineAAEnable" } for key, value in data['Viewport Options'].items(): # There are some keys we want to ignore if key in {"override_viewport_options", "high_quality"}: continue # First handle special cases where we do value conversion to # separate option values if key == 'textureMaxResolution': viewport2_options['textureMaxResolution'] = value if value > 0: viewport2_options['enableTextureMaxRes'] = True viewport2_options['textureMaxResMode'] = 1 else: viewport2_options['enableTextureMaxRes'] = False viewport2_options['textureMaxResMode'] = 0 elif key == 'multiSample': viewport2_options['multiSampleEnable'] = value > 0 viewport2_options['multiSampleCount'] = value elif key == 'alphaCut': viewport2_options['transparencyAlgorithm'] = 5 viewport2_options['transparencyQuality'] = 1 elif key == 'hwFogFalloff': # Settings enum value string to integer viewport2_options['hwFogFalloff'] = int(value) # Then handle Viewport 2.0 Options elif key in VIEWPORT2_OPTIONS: viewport2_options[key] = value # Then assume remainder is Viewport Options else: viewport_options[key] = value options['viewport_options'] = viewport_options options['viewport2_options'] = viewport2_options options['camera_options'] = camera_options # use active sound track scene = capture.parse_active_scene() options['sound'] = scene['sound'] return options def get_attr_in_layer(attr, layer): """Return attribute value in specified renderlayer. Same as cmds.getAttr but this gets the attribute's value in a given render layer without having to switch to it. Warning for parent attribute overrides: Attributes that have render layer overrides to their parent attribute are not captured correctly since they do not have a direct connection. For example, an override to sphere.rotate when querying sphere.rotateX will not return correctly! Note: This is much faster for Maya's renderLayer system, yet the code does no optimized query for render setup. Args: attr (str): attribute name, ex. "node.attribute" layer (str): layer name Returns: The return value from `maya.cmds.getAttr` """ try: if cmds.mayaHasRenderSetup(): from . import lib_rendersetup return lib_rendersetup.get_attr_in_layer(attr, layer) except AttributeError: pass # Ignore complex query if we're in the layer anyway current_layer = cmds.editRenderLayerGlobals(query=True, currentRenderLayer=True) if layer == current_layer: return cmds.getAttr(attr) connections = cmds.listConnections(attr, plugs=True, source=False, destination=True, type="renderLayer") or [] connections = filter(lambda x: x.endswith(".plug"), connections) if not connections: return cmds.getAttr(attr) # Some value types perform a conversion when assigning # TODO: See if there's a maya method to allow this conversion # instead of computing it ourselves. attr_type = cmds.getAttr(attr, type=True) conversion = None if attr_type == "time": conversion = mel.eval('currentTimeUnitToFPS()') # returns float elif attr_type == "doubleAngle": # Radians to Degrees: 180 / pi # TODO: This will likely only be correct when Maya units are set # to degrees conversion = 57.2957795131 elif attr_type == "doubleLinear": raise NotImplementedError("doubleLinear conversion not implemented.") for connection in connections: if connection.startswith(layer + "."): attr_split = connection.split(".") if attr_split[0] == layer: attr = ".".join(attr_split[0:-1]) value = cmds.getAttr("%s.value" % attr) if conversion: value *= conversion return value else: # When connections are present, but none # to the specific renderlayer than the layer # should have the "defaultRenderLayer"'s value layer = "defaultRenderLayer" for connection in connections: if connection.startswith(layer): attr_split = connection.split(".") if attr_split[0] == "defaultRenderLayer": attr = ".".join(attr_split[0:-1]) value = cmds.getAttr("%s.value" % attr) if conversion: value *= conversion return value return cmds.getAttr(attr) def fix_incompatible_containers(): """Backwards compatibility: old containers to use new ReferenceLoader""" old_loaders = { "MayaAsciiLoader", "AbcLoader", "ModelLoader", "CameraLoader", "RigLoader", "FBXLoader" } host = registered_host() for container in host.ls(): loader = container['loader'] if loader in old_loaders: log.info( "Converting legacy container loader {} to " "ReferenceLoader: {}".format(loader, container["objectName"]) ) cmds.setAttr(container["objectName"] + ".loader", "ReferenceLoader", type="string") def update_content_on_context_change(): """ This will update scene content to match new asset on context change """ scene_sets = cmds.listSets(allSets=True) asset_doc = get_current_project_asset() new_asset = asset_doc["name"] new_data = asset_doc["data"] for s in scene_sets: try: if cmds.getAttr("{}.id".format(s)) == "pyblish.avalon.instance": attr = cmds.listAttr(s) print(s) if "asset" in attr: print(" - setting asset to: [ {} ]".format(new_asset)) cmds.setAttr("{}.asset".format(s), new_asset, type="string") if "frameStart" in attr: cmds.setAttr("{}.frameStart".format(s), new_data["frameStart"]) if "frameEnd" in attr: cmds.setAttr("{}.frameEnd".format(s), new_data["frameEnd"],) except ValueError: pass def show_message(title, msg): from qtpy import QtWidgets from openpype.widgets import message_window # Find maya main window top_level_widgets = {w.objectName(): w for w in QtWidgets.QApplication.topLevelWidgets()} parent = top_level_widgets.get("MayaWindow", None) if parent is None: pass else: message_window.message(title=title, message=msg, parent=parent) def iter_shader_edits(relationships, shader_nodes, nodes_by_id, label=None): """Yield edits as a set of actions.""" attributes = relationships.get("attributes", []) shader_data = relationships.get("relationships", {}) shading_engines = cmds.ls(shader_nodes, type="objectSet", long=True) assert shading_engines, "Error in retrieving objectSets from reference" # region compute lookup shading_engines_by_id = defaultdict(list) for shad in shading_engines: shading_engines_by_id[get_id(shad)].append(shad) # endregion # region assign shading engines and other sets for data in shader_data.values(): # collect all unique IDs of the set members shader_uuid = data["uuid"] member_uuids = [ (member["uuid"], member.get("components")) for member in data["members"]] filtered_nodes = list() for _uuid, components in member_uuids: nodes = nodes_by_id.get(_uuid, None) if nodes is None: continue if components: # Assign to the components nodes = [".".join([node, components]) for node in nodes] filtered_nodes.extend(nodes) id_shading_engines = shading_engines_by_id[shader_uuid] if not id_shading_engines: log.error("{} - No shader found with cbId " "'{}'".format(label, shader_uuid)) continue elif len(id_shading_engines) > 1: log.error("{} - Skipping shader assignment. " "More than one shader found with cbId " "'{}'. (found: {})".format(label, shader_uuid, id_shading_engines)) continue if not filtered_nodes: log.warning("{} - No nodes found for shading engine " "'{}'".format(label, id_shading_engines[0])) continue yield {"action": "assign", "uuid": data["uuid"], "nodes": filtered_nodes, "shader": id_shading_engines[0]} for data in attributes: nodes = nodes_by_id.get(data["uuid"], []) attr_value = data["attributes"] yield {"action": "setattr", "uuid": data["uuid"], "nodes": nodes, "attributes": attr_value} def set_colorspace(): """Set Colorspace from project configuration""" project_name = get_current_project_name() imageio = get_project_settings(project_name)["maya"]["imageio"] # ocio compatibility variables ocio_v2_maya_version = 2022 maya_version = int(cmds.about(version=True)) ocio_v2_support = use_ocio_v2 = maya_version >= ocio_v2_maya_version is_ocio_set = bool(os.environ.get("OCIO")) use_workfile_settings = imageio.get("workfile", {}).get("enabled") if use_workfile_settings: root_dict = imageio["workfile"] else: # TODO: deprecated code from 3.15.5 - remove # Maya 2022+ introduces new OCIO v2 color management settings that # can override the old color management preferences. OpenPype has # separate settings for both so we fall back when necessary. use_ocio_v2 = imageio["colorManagementPreference_v2"]["enabled"] if use_ocio_v2 and not ocio_v2_support: # Fallback to legacy behavior with a warning log.warning( "Color Management Preference v2 is enabled but not " "supported by current Maya version: {} (< {}). Falling " "back to legacy settings.".format( maya_version, ocio_v2_maya_version) ) if use_ocio_v2: root_dict = imageio["colorManagementPreference_v2"] else: root_dict = imageio["colorManagementPreference"] if not isinstance(root_dict, dict): msg = "set_colorspace(): argument should be dictionary" log.error(msg) return # backward compatibility # TODO: deprecated code from 3.15.5 - remove with deprecated code above view_name = root_dict.get("viewTransform") if view_name is None: view_name = root_dict.get("viewName") log.debug(">> root_dict: {}".format(pformat(root_dict))) if not root_dict: return # set color spaces for rendering space and view transforms def _colormanage(**kwargs): """Wrapper around `cmds.colorManagementPrefs`. This logs errors instead of raising an error so color management settings get applied as much as possible. """ assert len(kwargs) == 1, "Must receive one keyword argument" try: cmds.colorManagementPrefs(edit=True, **kwargs) log.debug("Setting Color Management Preference: {}".format(kwargs)) except RuntimeError as exc: log.error(exc) # enable color management cmds.colorManagementPrefs(edit=True, cmEnabled=True) cmds.colorManagementPrefs(edit=True, ocioRulesEnabled=True) if use_ocio_v2: log.info("Using Maya OCIO v2") if not is_ocio_set: # Set the Maya 2022+ default OCIO v2 config file path log.info("Setting default Maya OCIO v2 config") # Note: Setting "" as value also sets this default however # introduces a bug where launching a file on startup will prompt # to save the empty scene before it, so we set using the path. # This value has been the same for 2022, 2023 and 2024 path = " /OCIO-configs/Maya2022-default/config.ocio" cmds.colorManagementPrefs(edit=True, configFilePath=path) # set rendering space and view transform _colormanage(renderingSpaceName=root_dict["renderSpace"]) _colormanage(viewName=view_name) _colormanage(displayName=root_dict["displayName"]) else: log.info("Using Maya OCIO v1 (legacy)") if not is_ocio_set: # Set the Maya default config file path log.info("Setting default Maya OCIO v1 legacy config") cmds.colorManagementPrefs(edit=True, configFilePath="legacy") # set rendering space and view transform _colormanage(renderingSpaceName=root_dict["renderSpace"]) _colormanage(viewTransformName=view_name) @contextlib.contextmanager def parent_nodes(nodes, parent=None): # type: (list, str) -> list """Context manager to un-parent provided nodes and return them back.""" def _as_mdagpath(node): """Return MDagPath for node path.""" if not node: return sel = OpenMaya.MSelectionList() sel.add(node) return sel.getDagPath(0) # We can only parent dag nodes so we ensure input contains only dag nodes nodes = cmds.ls(nodes, type="dagNode", long=True) if not nodes: # opt-out early yield return parent_node_path = None delete_parent = False if parent: if not cmds.objExists(parent): parent_node = cmds.createNode("transform", name=parent, skipSelect=False) delete_parent = True else: parent_node = parent parent_node_path = cmds.ls(parent_node, long=True)[0] # Store original parents node_parents = [] for node in nodes: node_parent = get_node_parent(node) node_parents.append((_as_mdagpath(node), _as_mdagpath(node_parent))) try: for node, node_parent in node_parents: node_parent_path = node_parent.fullPathName() if node_parent else None # noqa if node_parent_path == parent_node_path: # Already a child continue if parent_node_path: cmds.parent(node.fullPathName(), parent_node_path) else: cmds.parent(node.fullPathName(), world=True) yield finally: # Reparent to original parents for node, original_parent in node_parents: node_path = node.fullPathName() if not node_path: # Node must have been deleted continue node_parent_path = get_node_parent(node_path) original_parent_path = None if original_parent: original_parent_path = original_parent.fullPathName() if not original_parent_path: # Original parent node must have been deleted continue if node_parent_path != original_parent_path: if not original_parent_path: cmds.parent(node_path, world=True) else: cmds.parent(node_path, original_parent_path) if delete_parent: cmds.delete(parent_node_path) @contextlib.contextmanager def maintained_time(): ct = cmds.currentTime(query=True) try: yield finally: cmds.currentTime(ct, edit=True) def iter_visible_nodes_in_range(nodes, start, end): """Yield nodes that are visible in start-end frame range. - Ignores intermediateObjects completely. - Considers animated visibility attributes + upstream visibilities. This is optimized for large scenes where some nodes in the parent hierarchy might have some input connections to the visibilities, e.g. key, driven keys, connections to other attributes, etc. This only does a single time step to `start` if current frame is not inside frame range since the assumption is made that changing a frame isn't so slow that it beats querying all visibility plugs through MDGContext on another frame. Args: nodes (list): List of node names to consider. start (int, float): Start frame. end (int, float): End frame. Returns: list: List of node names. These will be long full path names so might have a longer name than the input nodes. """ # States we consider per node VISIBLE = 1 # always visible INVISIBLE = 0 # always invisible ANIMATED = -1 # animated visibility # Ensure integers start = int(start) end = int(end) # Consider only non-intermediate dag nodes and use the "long" names. nodes = cmds.ls(nodes, long=True, noIntermediate=True, type="dagNode") if not nodes: return with maintained_time(): # Go to first frame of the range if the current time is outside # the queried range so can directly query all visible nodes on # that frame. current_time = cmds.currentTime(query=True) if not (start <= current_time <= end): cmds.currentTime(start) visible = cmds.ls(nodes, long=True, visible=True) for node in visible: yield node if len(visible) == len(nodes) or start == end: # All are visible on frame one, so they are at least visible once # inside the frame range. return # For the invisible ones check whether its visibility and/or # any of its parents visibility attributes are animated. If so, it might # get visible on other frames in the range. def memodict(f): """Memoization decorator for a function taking a single argument. See: http://code.activestate.com/recipes/ 578231-probably-the-fastest-memoization-decorator-in-the-/ """ class memodict(dict): def __missing__(self, key): ret = self[key] = f(key) return ret return memodict().__getitem__ @memodict def get_state(node): plug = node + ".visibility" connections = cmds.listConnections(plug, source=True, destination=False) if connections: return ANIMATED else: return VISIBLE if cmds.getAttr(plug) else INVISIBLE visible = set(visible) invisible = [node for node in nodes if node not in visible] always_invisible = set() # Iterate over the nodes by short to long names to iterate the highest # in hierarchy nodes first. So the collected data can be used from the # cache for parent queries in next iterations. node_dependencies = dict() for node in sorted(invisible, key=len): state = get_state(node) if state == INVISIBLE: always_invisible.add(node) continue # If not always invisible by itself we should go through and check # the parents to see if any of them are always invisible. For those # that are "ANIMATED" we consider that this node is dependent on # that attribute, we store them as dependency. dependencies = set() if state == ANIMATED: dependencies.add(node) traversed_parents = list() for parent in iter_parents(node): if parent in always_invisible or get_state(parent) == INVISIBLE: # When parent is always invisible then consider this parent, # this node we started from and any of the parents we # have traversed in-between to be *always invisible* always_invisible.add(parent) always_invisible.add(node) always_invisible.update(traversed_parents) break # If we have traversed the parent before and its visibility # was dependent on animated visibilities then we can just extend # its dependencies for to those for this node and break further # iteration upwards. parent_dependencies = node_dependencies.get(parent, None) if parent_dependencies is not None: dependencies.update(parent_dependencies) break state = get_state(parent) if state == ANIMATED: dependencies.add(parent) traversed_parents.append(parent) if node not in always_invisible and dependencies: node_dependencies[node] = dependencies if not node_dependencies: return # Now we only have to check the visibilities for nodes that have animated # visibility dependencies upstream. The fastest way to check these # visibility attributes across different frames is with Python api 2.0 # so we do that. @memodict def get_visibility_mplug(node): """Return api 2.0 MPlug with cached memoize decorator""" sel = OpenMaya.MSelectionList() sel.add(node) dag = sel.getDagPath(0) return OpenMaya.MFnDagNode(dag).findPlug("visibility", True) @contextlib.contextmanager def dgcontext(mtime): """MDGContext context manager""" context = OpenMaya.MDGContext(mtime) try: previous = context.makeCurrent() yield context finally: previous.makeCurrent() # We skip the first frame as we already used that frame to check for # overall visibilities. And end+1 to include the end frame. scene_units = OpenMaya.MTime.uiUnit() for frame in range(start + 1, end + 1): mtime = OpenMaya.MTime(frame, unit=scene_units) # Build little cache so we don't query the same MPlug's value # again if it was checked on this frame and also is a dependency # for another node frame_visibilities = {} with dgcontext(mtime) as context: for node, dependencies in list(node_dependencies.items()): for dependency in dependencies: dependency_visible = frame_visibilities.get(dependency, None) if dependency_visible is None: mplug = get_visibility_mplug(dependency) dependency_visible = mplug.asBool(context) frame_visibilities[dependency] = dependency_visible if not dependency_visible: # One dependency is not visible, thus the # node is not visible. break else: # All dependencies are visible. yield node # Remove node with dependencies for next frame iterations # because it was visible at least once. node_dependencies.pop(node) # If no more nodes to process break the frame iterations.. if not node_dependencies: break def get_attribute_input(attr): connections = cmds.listConnections(attr, plugs=True, destination=False) return connections[0] if connections else None def convert_to_maya_fps(fps): """Convert any fps to supported Maya framerates.""" float_framerates = [ 23.976023976023978, # WTF is 29.97 df vs fps? 29.97002997002997, 47.952047952047955, 59.94005994005994 ] # 44100 fps evaluates as 41000.0. Why? Omitting for now. int_framerates = [ 2, 3, 4, 5, 6, 8, 10, 12, 15, 16, 20, 24, 25, 30, 40, 48, 50, 60, 75, 80, 90, 100, 120, 125, 150, 200, 240, 250, 300, 375, 400, 500, 600, 750, 1200, 1500, 2000, 3000, 6000, 48000 ] # If input fps is a whole number we'll return. if float(fps).is_integer(): # Validate fps is part of Maya's fps selection. if int(fps) not in int_framerates: raise ValueError( "Framerate \"{}\" is not supported in Maya".format(fps) ) return int(fps) else: # Differences to supported float frame rates. differences = [] for i in float_framerates: differences.append(abs(i - fps)) # Validate difference does not stray too far from supported framerates. min_difference = min(differences) min_index = differences.index(min_difference) supported_framerate = float_framerates[min_index] if min_difference > 0.1: raise ValueError( "Framerate \"{}\" strays too far from any supported framerate" " in Maya. Closest supported framerate is \"{}\"".format( fps, supported_framerate ) ) return supported_framerate def write_xgen_file(data, filepath): """Overwrites data in .xgen files. Quite naive approach to mainly overwrite "xgDataPath" and "xgProjectPath". Args: data (dict): Dictionary of key, value. Key matches with xgen file. For example: {"xgDataPath": "some/path"} filepath (string): Absolute path of .xgen file. """ # Generate regex lookup for line to key basically # match any of the keys in `\t{key}\t\t` keys = "|".join(re.escape(key) for key in data.keys()) re_keys = re.compile("^\t({})\t\t".format(keys)) lines = [] with open(filepath, "r") as f: for line in f: match = re_keys.match(line) if match: key = match.group(1) value = data[key] line = "\t{}\t\t{}\n".format(key, value) lines.append(line) with open(filepath, "w") as f: f.writelines(lines) def get_color_management_preferences(): """Get and resolve OCIO preferences.""" data = { # Is color management enabled. "enabled": cmds.colorManagementPrefs( query=True, cmEnabled=True ), "rendering_space": cmds.colorManagementPrefs( query=True, renderingSpaceName=True ), "output_transform": cmds.colorManagementPrefs( query=True, outputTransformName=True ), "output_transform_enabled": cmds.colorManagementPrefs( query=True, outputTransformEnabled=True ), "view_transform": cmds.colorManagementPrefs( query=True, viewTransformName=True ) } # Split view and display from view_transform. view_transform comes in # format of "{view} ({display})". regex = re.compile(r"^(?P .+) \((?P .+)\)$") if int(cmds.about(version=True)) <= 2020: # view_transform comes in format of "{view} {display}" in 2020. regex = re.compile(r"^(?P .+) (?P .+)$") match = regex.match(data["view_transform"]) if not match: raise ValueError( "Unable to parse view and display from Maya view transform: '{}' " "using regex '{}'".format(data["view_transform"], regex.pattern) ) data.update({ "display": match.group("display"), "view": match.group("view") }) # Get config absolute path. path = cmds.colorManagementPrefs( query=True, configFilePath=True ) # The OCIO config supports a custom token. maya_resources_token = " " maya_resources_path = OpenMaya.MGlobal.getAbsolutePathToResources() path = path.replace(maya_resources_token, maya_resources_path) data["config"] = path return data def get_color_management_output_transform(): preferences = get_color_management_preferences() colorspace = preferences["rendering_space"] if preferences["output_transform_enabled"]: colorspace = preferences["output_transform"] return colorspace def image_info(file_path): # type: (str) -> dict """Based on tha texture path, get its bit depth and format information. Take reference from makeTx.py in Arnold: ImageInfo(filename): Get Image Information for colorspace AiTextureGetFormat(filename): Get Texture Format AiTextureGetBitDepth(filename): Get Texture bit depth Args: file_path (str): Path to the texture file. Returns: dict: Dictionary with the information about the texture file. """ from arnold import ( AiTextureGetBitDepth, AiTextureGetFormat ) # Get Texture Information img_info = {'filename': file_path} if os.path.isfile(file_path): img_info['bit_depth'] = AiTextureGetBitDepth(file_path) # noqa img_info['format'] = AiTextureGetFormat(file_path) # noqa else: img_info['bit_depth'] = 8 img_info['format'] = "unknown" return img_info def guess_colorspace(img_info): # type: (dict) -> str """Guess the colorspace of the input image filename. Note: Reference from makeTx.py Args: img_info (dict): Image info generated by :func:`image_info` Returns: str: color space name use in the `--colorconvert` option of maketx. """ from arnold import ( AiTextureInvalidate, # types AI_TYPE_BYTE, AI_TYPE_INT, AI_TYPE_UINT ) try: if img_info['bit_depth'] <= 16: if img_info['format'] in (AI_TYPE_BYTE, AI_TYPE_INT, AI_TYPE_UINT): # noqa return 'sRGB' else: return 'linear' # now discard the image file as AiTextureGetFormat has loaded it AiTextureInvalidate(img_info['filename']) # noqa except ValueError: print(("[maketx] Error: Could not guess" "colorspace for {}").format(img_info["filename"])) return "linear" def len_flattened(components): """Return the length of the list as if it was flattened. Maya will return consecutive components as a single entry when requesting with `maya.cmds.ls` without the `flatten` flag. Though enabling `flatten` on a large list (e.g. millions) will result in a slow result. This command will return the amount of entries in a non-flattened list by parsing the result with regex. Args: components (list): The non-flattened components. Returns: int: The amount of entries. """ assert isinstance(components, (list, tuple)) n = 0 pattern = re.compile(r"\[(\d+):(\d+)\]") for c in components: match = pattern.search(c) if match: start, end = match.groups() n += int(end) - int(start) + 1 else: n += 1 return n def get_all_children(nodes): """Return all children of `nodes` including each instanced child. Using maya.cmds.listRelatives(allDescendents=True) includes only the first instance. As such, this function acts as an optimal replacement with a focus on a fast query. """ sel = OpenMaya.MSelectionList() traversed = set() iterator = OpenMaya.MItDag(OpenMaya.MItDag.kDepthFirst) for node in nodes: if node in traversed: # Ignore if already processed as a child # before continue sel.clear() sel.add(node) dag = sel.getDagPath(0) iterator.reset(dag) # ignore self iterator.next() # noqa: B305 while not iterator.isDone(): path = iterator.fullPathName() if path in traversed: iterator.prune() iterator.next() # noqa: B305 continue traversed.add(path) iterator.next() # noqa: B305 return list(traversed) def get_capture_preset(task_name, task_type, subset, project_settings, log): """Get capture preset for playblasting. Logic for transitioning from old style capture preset to new capture preset profiles. Args: task_name (str): Task name. take_type (str): Task type. subset (str): Subset name. project_settings (dict): Project settings. log (object): Logging object. """ capture_preset = None filtering_criteria = { "hosts": "maya", "families": "review", "task_names": task_name, "task_types": task_type, "subset": subset } plugin_settings = project_settings["maya"]["publish"]["ExtractPlayblast"] if plugin_settings["profiles"]: profile = filter_profiles( plugin_settings["profiles"], filtering_criteria, logger=log ) capture_preset = profile.get("capture_preset") else: log.warning("No profiles present for Extract Playblast") # Backward compatibility for deprecated Extract Playblast settings # without profiles. if capture_preset is None: log.debug( "Falling back to deprecated Extract Playblast capture preset " "because no new style playblast profiles are defined." ) capture_preset = plugin_settings["capture_preset"] return capture_preset or {} def get_reference_node(members, log=None): """Get the reference node from the container members Args: members: list of node names Returns: str: Reference node name. """ # Collect the references without .placeHolderList[] attributes as # unique entries (objects only) and skipping the sharedReferenceNode. references = set() for ref in cmds.ls(members, exactType="reference", objectsOnly=True): # Ignore any `:sharedReferenceNode` if ref.rsplit(":", 1)[-1].startswith("sharedReferenceNode"): continue # Ignore _UNKNOWN_REF_NODE_ (PLN-160) if ref.rsplit(":", 1)[-1].startswith("_UNKNOWN_REF_NODE_"): continue references.add(ref) assert references, "No reference node found in container" # Get highest reference node (least parents) highest = min(references, key=lambda x: len(get_reference_node_parents(x))) # Warn the user when we're taking the highest reference node if len(references) > 1: if not log: log = logging.getLogger(__name__) log.warning("More than one reference node found in " "container, using highest reference node: " "%s (in: %s)", highest, list(references)) return highest def get_reference_node_parents(ref): """Return all parent reference nodes of reference node Args: ref (str): reference node. Returns: list: The upstream parent reference nodes. """ parent = cmds.referenceQuery(ref, referenceNode=True, parent=True) parents = [] while parent: parents.append(parent) parent = cmds.referenceQuery(parent, referenceNode=True, parent=True) return parents def create_rig_animation_instance( nodes, context, namespace, options=None, log=None ): """Create an animation publish instance for loaded rigs. See the RecreateRigAnimationInstance inventory action on how to use this for loaded rig containers. Arguments: nodes (list): Member nodes of the rig instance. context (dict): Representation context of the rig container namespace (str): Namespace of the rig container options (dict, optional): Additional loader data log (logging.Logger, optional): Logger to log to if provided Returns: None """ if options is None: options = {} name = context["representation"]["name"] output = next((node for node in nodes if node.endswith("out_SET")), None) controls = next((node for node in nodes if node.endswith("controls_SET")), None) if name != "fbx": assert output, "No out_SET in rig, this is a bug." assert controls, "No controls_SET in rig, this is a bug." anim_skeleton = next((node for node in nodes if node.endswith("skeletonAnim_SET")), None) skeleton_mesh = next((node for node in nodes if node.endswith("skeletonMesh_SET")), None) # Find the roots amongst the loaded nodes roots = ( cmds.ls(nodes, assemblies=True, long=True) or get_highest_in_hierarchy(nodes) ) assert roots, "No root nodes in rig, this is a bug." custom_subset = options.get("animationSubsetName") if custom_subset: formatting_data = { "asset": context["asset"], "subset": context['subset']['name'], "family": ( context['subset']['data'].get('family') or context['subset']['data']['families'][0] ) } namespace = get_custom_namespace( custom_subset.format( **formatting_data ) ) if log: log.info("Creating subset: {}".format(namespace)) # Fill creator identifier creator_identifier = "io.openpype.creators.maya.animation" host = registered_host() create_context = CreateContext(host) # Create the animation instance rig_sets = [output, controls, anim_skeleton, skeleton_mesh] # Remove sets that this particular rig does not have rig_sets = [s for s in rig_sets if s is not None] with maintained_selection(): cmds.select(rig_sets + roots, noExpand=True) create_context.create( creator_identifier=creator_identifier, variant=namespace, pre_create_data={"use_selection": True} ) ================================================ FILE: openpype/hosts/maya/api/lib_renderproducts.py ================================================ # -*- coding: utf-8 -*- """Module handling expected render output from Maya. This module is used in :mod:`collect_render` and :mod:`collect_vray_scene`. Note: To implement new renderer, just create new class inheriting from :class:`ARenderProducts` and add it to :func:`RenderProducts.get()`. Attributes: R_SINGLE_FRAME (:class:`re.Pattern`): Find single frame number. R_FRAME_RANGE (:class:`re.Pattern`): Find frame range. R_FRAME_NUMBER (:class:`re.Pattern`): Find frame number in string. R_LAYER_TOKEN (:class:`re.Pattern`): Find layer token in image prefixes. R_AOV_TOKEN (:class:`re.Pattern`): Find AOV token in image prefixes. R_SUBSTITUTE_AOV_TOKEN (:class:`re.Pattern`): Find and substitute AOV token in image prefixes. R_REMOVE_AOV_TOKEN (:class:`re.Pattern`): Find and remove AOV token in image prefixes. R_CLEAN_FRAME_TOKEN (:class:`re.Pattern`): Find and remove unfilled Renderman frame token in image prefix. R_CLEAN_EXT_TOKEN (:class:`re.Pattern`): Find and remove unfilled Renderman extension token in image prefix. R_SUBSTITUTE_LAYER_TOKEN (:class:`re.Pattern`): Find and substitute render layer token in image prefixes. R_SUBSTITUTE_SCENE_TOKEN (:class:`re.Pattern`): Find and substitute scene token in image prefixes. R_SUBSTITUTE_CAMERA_TOKEN (:class:`re.Pattern`): Find and substitute camera token in image prefixes. IMAGE_PREFIXES (dict): Mapping between renderers and their respective image prefix attribute names. Thanks: Roy Nieterau (BigRoy) / Colorbleed for overhaul of original *expected_files*. """ import logging import re import os from abc import ABCMeta, abstractmethod import six import attr from . import lib from . import lib_rendersetup from openpype.pipeline.colorspace import get_ocio_config_views from maya import cmds, mel log = logging.getLogger(__name__) R_SINGLE_FRAME = re.compile(r"^(-?)\d+$") R_FRAME_RANGE = re.compile(r"^(?P (-?)\d+)-(?P (-?)\d+)$") R_FRAME_NUMBER = re.compile(r".+\.(?P[0-9]+)\..+") R_LAYER_TOKEN = re.compile( r".*((?:%l)|(?: )|(?: )).*", re.IGNORECASE ) R_AOV_TOKEN = re.compile(r".*%a.*|.* .*|.* .*", re.IGNORECASE) R_SUBSTITUTE_AOV_TOKEN = re.compile(r"%a| | ", re.IGNORECASE) R_REMOVE_AOV_TOKEN = re.compile( r"_%a|\.%a|_ |\. |_ |\. ", re.IGNORECASE) # to remove unused renderman tokens R_CLEAN_FRAME_TOKEN = re.compile(r"\.? \.?", re.IGNORECASE) R_CLEAN_EXT_TOKEN = re.compile(r"\.? \.?", re.IGNORECASE) R_SUBSTITUTE_LAYER_TOKEN = re.compile( r"%l| | ", re.IGNORECASE ) R_SUBSTITUTE_CAMERA_TOKEN = re.compile(r"%c| ", re.IGNORECASE) R_SUBSTITUTE_SCENE_TOKEN = re.compile(r"%s| ", re.IGNORECASE) # not sure about the renderman image prefix IMAGE_PREFIXES = { "vray": "vraySettings.fileNamePrefix", "arnold": "defaultRenderGlobals.imageFilePrefix", "renderman": "rmanGlobals.imageFileFormat", "redshift": "defaultRenderGlobals.imageFilePrefix", "mayahardware2": "defaultRenderGlobals.imageFilePrefix" } RENDERMAN_IMAGE_DIR = " / " def has_tokens(string, tokens): """Return whether any of tokens is in input string (case-insensitive)""" pattern = "({})".format("|".join(re.escape(token) for token in tokens)) match = re.search(pattern, string, re.IGNORECASE) return bool(match) @attr.s class LayerMetadata(object): """Data class for Render Layer metadata.""" frameStart = attr.ib() frameEnd = attr.ib() cameras = attr.ib() sceneName = attr.ib() layerName = attr.ib() renderer = attr.ib() defaultExt = attr.ib() filePrefix = attr.ib() frameStep = attr.ib(default=1) padding = attr.ib(default=4) # Render Products products = attr.ib(init=False, default=attr.Factory(list)) # The AOV separator token. Note that not all renderers define an explicit # render separator but allow to put the AOV/RenderPass token anywhere in # the file path prefix. For those renderers we'll fall back to whatever # is between the last occurrences of and tokens. aov_separator = attr.ib(default="_") @attr.s class RenderProduct(object): """Describes an image or other file-like artifact produced by a render. Warning: This currently does NOT return as a product PER render camera. A single Render Product will generate files per camera. E.g. with two cameras each render product generates two sequences on disk assuming the file path prefix correctly uses the tokens. """ productName = attr.ib() ext = attr.ib() # extension colorspace = attr.ib() # colorspace aov = attr.ib(default=None) # source aov driver = attr.ib(default=None) # source driver multipart = attr.ib(default=False) # multichannel file camera = attr.ib(default=None) # used only when rendering # from multiple cameras def get(layer, render_instance=None): # type: (str, object) -> ARenderProducts """Get render details and products for given renderer and render layer. Args: layer (str): Name of render layer render_instance (pyblish.api.Instance): Publish instance. If not provided an empty mock instance is used. Returns: ARenderProducts: The correct RenderProducts instance for that renderlayer. Raises: :exc:`UnsupportedRendererException`: If requested renderer is not supported. It needs to be implemented by extending :class:`ARenderProducts` and added to this methods ``if`` statement. """ if render_instance is None: # For now produce a mock instance class Instance(object): data = {} render_instance = Instance() renderer_name = lib.get_attr_in_layer( "defaultRenderGlobals.currentRenderer", layer=layer ) renderer = { "arnold": RenderProductsArnold, "vray": RenderProductsVray, "redshift": RenderProductsRedshift, "renderman": RenderProductsRenderman, "mayahardware2": RenderProductsMayaHardware }.get(renderer_name.lower(), None) if renderer is None: raise UnsupportedRendererException( "Unsupported renderer: {}".format(renderer_name) ) return renderer(layer, render_instance) @six.add_metaclass(ABCMeta) class ARenderProducts: """Abstract class with common code for all renderers. Attributes: renderer (str): name of renderer. """ renderer = None def __init__(self, layer, render_instance): """Constructor.""" self.layer = layer self.render_instance = render_instance self.multipart = self.get_multipart() # Initialize self.layer_data = self._get_layer_data() self.layer_data.products = self.get_render_products() def get_multipart(self): raise NotImplementedError( "The render product implementation does not have a " "\"get_multipart\" method." ) def has_camera_token(self): # type: () -> bool """Check if camera token is in image prefix. Returns: bool: True/False if camera token is present. """ return " " in self.layer_data.filePrefix.lower() @abstractmethod def get_render_products(self): """To be implemented by renderer class. This should return a list of RenderProducts. Returns: list: List of RenderProduct """ @staticmethod def sanitize_camera_name(camera): # type: (str) -> str """Sanitize camera name. Remove Maya illegal characters from camera name. Args: camera (str): Maya camera name. Returns: (str): sanitized camera name Example: >>> ARenderProducts.sanizite_camera_name('test:camera_01') test_camera_01 """ return re.sub('[^0-9a-zA-Z_]+', '_', camera) def get_renderer_prefix(self): # type: () -> str """Return prefix for specific renderer. This is for most renderers the same and can be overridden if needed. Returns: str: String with image prefix containing tokens Raises: :exc:`UnsupportedRendererException`: If we requested image prefix for renderer we know nothing about. See :data:`IMAGE_PREFIXES` for mapping of renderers and image prefixes. """ try: prefix_attr = IMAGE_PREFIXES[self.renderer] except KeyError: raise UnsupportedRendererException( "Unsupported renderer {}".format(self.renderer) ) # Note: When this attribute is never set (e.g. on maya launch) then # this can return None even though it is a string attribute prefix = self._get_attr(prefix_attr) if not prefix: # Fall back to scene name by default log.warning("Image prefix not set, using ") prefix = " " return prefix def get_render_attribute(self, attribute): """Get attribute from render options. Args: attribute (str): name of attribute to be looked up. Returns: Attribute value """ return self._get_attr("defaultRenderGlobals", attribute) def _get_attr(self, node_attr, attribute=None): """Return the value of the attribute in the renderlayer For readability this allows passing in the attribute in two ways. As a single argument: _get_attr("node.attr") Or as two arguments: _get_attr("node", "attr") Returns: Value of the attribute inside the layer this instance is set to. """ if attribute is None: plug = node_attr else: plug = "{}.{}".format(node_attr, attribute) return lib.get_attr_in_layer(plug, layer=self.layer) @staticmethod def extract_separator(file_prefix): """Extract AOV separator character from the prefix. Default behavior extracts the part between last occurrences of and Todo: This code also triggers for V-Ray which overrides it explicitly so this code will invalidly debug log it couldn't extract the AOV separator even though it does set it in RenderProductsVray. Args: file_prefix (str): File prefix with tokens. Returns: str or None: prefix character if it can be extracted. """ layer_tokens = [" ", " "] aov_tokens = [" ", " "] def match_last(tokens, text): """regex match the last occurrence from a list of tokens""" pattern = "(?:.*)({})".format("|".join(tokens)) return re.search(pattern, text, re.IGNORECASE) layer_match = match_last(layer_tokens, file_prefix) aov_match = match_last(aov_tokens, file_prefix) separator = None if layer_match and aov_match: matches = sorted((layer_match, aov_match), key=lambda match: match.end(1)) separator = file_prefix[matches[0].end(1):matches[1].start(1)] return separator def _get_layer_data(self): # type: () -> LayerMetadata # ______________________________________________ # ____________________/ ____________________________________________/ # 1 - get scene name /__________________/ # ____________________/ _, scene_basename = os.path.split(cmds.file(q=True, loc=True)) scene_name, _ = os.path.splitext(scene_basename) kwargs = {} file_prefix = self.get_renderer_prefix() # If the Render Layer belongs to a Render Setup layer then the # output name is based on the Render Setup Layer name without # the `rs_` prefix. layer_name = self.layer rs_layer = lib_rendersetup.get_rendersetup_layer(layer_name) if rs_layer: layer_name = rs_layer if self.layer == "defaultRenderLayer": # defaultRenderLayer renders as masterLayer layer_name = "masterLayer" separator = self.extract_separator(file_prefix) if separator: kwargs["aov_separator"] = separator else: log.debug("Couldn't extract aov separator from " "file prefix: {}".format(file_prefix)) # todo: Support Custom Frames sequences 0,5-10,100-120 # Deadline allows submitting renders with a custom frame list # to support those cases we might want to allow 'custom frames' # to be overridden to `ExpectFiles` class? return LayerMetadata( frameStart=int(self.get_render_attribute("startFrame")), frameEnd=int(self.get_render_attribute("endFrame")), frameStep=int(self.get_render_attribute("byFrameStep")), padding=int(self.get_render_attribute("extensionPadding")), # if we have token in prefix path we'll expect output for # every renderable camera in layer. cameras=self.get_renderable_cameras(), sceneName=scene_name, layerName=layer_name, renderer=self.renderer, defaultExt=self._get_attr("defaultRenderGlobals.imfPluginKey"), filePrefix=file_prefix, **kwargs ) def _generate_file_sequence( self, layer_data, force_aov_name=None, force_ext=None, force_cameras=None): # type: (LayerMetadata, str, str, list) -> list expected_files = [] cameras = force_cameras or layer_data.cameras ext = force_ext or layer_data.defaultExt for cam in cameras: file_prefix = layer_data.filePrefix mappings = ( (R_SUBSTITUTE_SCENE_TOKEN, layer_data.sceneName), (R_SUBSTITUTE_LAYER_TOKEN, layer_data.layerName), (R_SUBSTITUTE_CAMERA_TOKEN, self.sanitize_camera_name(cam)), # this is required to remove unfilled aov token, for example # in Redshift (R_REMOVE_AOV_TOKEN, "") if not force_aov_name \ else (R_SUBSTITUTE_AOV_TOKEN, force_aov_name), (R_CLEAN_FRAME_TOKEN, ""), (R_CLEAN_EXT_TOKEN, ""), ) for regex, value in mappings: file_prefix = re.sub(regex, value, file_prefix) for frame in range( int(layer_data.frameStart), int(layer_data.frameEnd) + 1, int(layer_data.frameStep), ): frame_str = str(frame).rjust(layer_data.padding, "0") expected_files.append( "{}.{}.{}".format(file_prefix, frame_str, ext) ) return expected_files def get_files(self, product): # type: (RenderProduct) -> list """Return list of expected files. It will translate render token strings (' ', etc.) to their values. This task is tricky as every renderer deals with this differently. That's why we expose `get_files` as a method on the Renderer class so it can be overridden for complex cases. Args: product (RenderProduct): Render product to be used for file generation. Returns: List of files """ return self._generate_file_sequence( self.layer_data, force_aov_name=product.productName, force_ext=product.ext, force_cameras=[product.camera] ) def get_renderable_cameras(self): # type: () -> list """Get all renderable camera transforms. Returns: list: list of renderable cameras. """ renderable_cameras = [ cam for cam in cmds.ls(cameras=True) if self._get_attr(cam, "renderable") ] # The output produces a sanitized name for using its # shortest unique path of the transform so we'll return # at least that unique path. This could include a parent # name too when two cameras have the same name but are # in a different hierarchy, e.g. "group1|cam" and "group2|cam" def get_name(camera): return cmds.ls(cmds.listRelatives(camera, parent=True, fullPath=True))[0] return [get_name(cam) for cam in renderable_cameras] class RenderProductsArnold(ARenderProducts): """Render products for Arnold renderer. References: mtoa.utils.getFileName() mtoa.utils.ui.common.updateArnoldTargetFilePreview() Notes: - Output Denoising AOVs are not currently included. - Only Frame/Animation ext: name.#.ext is supported. - Use Custom extension is not supported. - and tokens not tested - With Merge AOVs but in File Name Prefix Arnold will still NOT merge the aovs. This class correctly resolves it - but user should be aware. - File Path Prefix overrides per AOV driver are not implemented Attributes: aiDriverExtension (dict): Arnold AOV driver extension mapping. Is there a better way? renderer (str): name of renderer. """ renderer = "arnold" aiDriverExtension = { "jpeg": "jpg", "exr": "exr", "deepexr": "exr", "png": "png", "tiff": "tif", "mtoa_shaders": "ass", # TODO: research what those last two should be "maya": "", } def get_renderer_prefix(self): prefix = super(RenderProductsArnold, self).get_renderer_prefix() merge_aovs = self._get_attr("defaultArnoldDriver.mergeAOVs") if not merge_aovs and " " not in prefix.lower(): # When Merge AOVs is disabled and token not present # then Arnold prepends / to the output path. # todo: It's untested what happens if AOV driver has an # an explicit override path prefix. prefix = " /" + prefix return prefix def get_multipart(self): multipart = False multilayer = bool(self._get_attr("defaultArnoldDriver.multipart")) merge_AOVs = bool(self._get_attr("defaultArnoldDriver.mergeAOVs")) if multilayer or merge_AOVs: multipart = True return multipart def _get_aov_render_products(self, aov, cameras=None): """Return all render products for the AOV""" products = [] aov_name = self._get_attr(aov, "name") ai_drivers = cmds.listConnections("{}.outputs".format(aov), source=True, destination=False, type="aiAOVDriver") or [] if not cameras: cameras = [ self.sanitize_camera_name( self.get_renderable_cameras()[0] ) ] for ai_driver in ai_drivers: colorspace = self._get_colorspace( ai_driver + ".colorManagement" ) # todo: check aiAOVDriver.prefix as it could have # a custom path prefix set for this driver # Skip Drivers set only for GUI # 0: GUI, 1: Batch, 2: GUI and Batch output_mode = self._get_attr(ai_driver, "outputMode") if output_mode == 0: # GUI only log.warning("%s has Output Mode set to GUI, " "skipping...", ai_driver) continue ai_translator = self._get_attr(ai_driver, "aiTranslator") try: ext = self.aiDriverExtension[ai_translator] except KeyError: raise AOVError( "Unrecognized arnold driver format " "for AOV - {}".format(aov_name) ) # If aov RGBA is selected, arnold will translate it to `beauty` name = aov_name if name == "RGBA": name = "beauty" # Support Arnold light groups for AOVs # Global AOV: When disabled the main layer is # not written: `{pass}` # All Light Groups: When enabled, a `{pass}_lgroups` file is # written and is always merged into a # single file # Light Groups List: When set, a product per light # group is written # e.g. {pass}_front, {pass}_rim global_aov = self._get_attr(aov, "globalAov") if global_aov: for camera in cameras: product = RenderProduct( productName=name, ext=ext, aov=aov_name, driver=ai_driver, multipart=self.multipart, camera=camera, colorspace=colorspace ) products.append(product) all_light_groups = self._get_attr(aov, "lightGroups") if all_light_groups: # All light groups is enabled. A single multipart # Render Product for camera in cameras: product = RenderProduct( productName=name + "_lgroups", ext=ext, aov=aov_name, driver=ai_driver, # Always multichannel output multipart=True, camera=camera, colorspace=colorspace ) products.append(product) else: value = self._get_attr(aov, "lightGroupsList") if not value: continue selected_light_groups = value.strip().split() for light_group in selected_light_groups: # Render Product per selected light group aov_light_group_name = "{}_{}".format(name, light_group) for camera in cameras: product = RenderProduct( productName=aov_light_group_name, aov=aov_name, driver=ai_driver, ext=ext, camera=camera, colorspace=colorspace ) products.append(product) return products def _get_colorspace(self, attribute): """Resolve colorspace from Arnold settings.""" def _view_transform(): preferences = lib.get_color_management_preferences() views_data = get_ocio_config_views(preferences["config"]) view_data = views_data[ "{}/{}".format(preferences["display"], preferences["view"]) ] return view_data["colorspace"] def _raw(): preferences = lib.get_color_management_preferences() return preferences["rendering_space"] resolved_values = { "Raw": _raw, "Use View Transform": _view_transform, # Default. Same as Maya Preferences. "Use Output Transform": lib.get_color_management_output_transform } return resolved_values[self._get_attr(attribute)]() def get_render_products(self): """Get all AOVs. See Also: :func:`ARenderProducts.get_render_products()` Raises: :class:`AOVError`: If AOV cannot be determined. """ if not cmds.ls("defaultArnoldRenderOptions", type="aiOptions"): # this occurs when Render Setting windows was not opened yet. In # such case there are no Arnold options created so query for AOVs # will fail. We terminate here as there are no AOVs specified then. # This state will most probably fail later on some Validator # anyway. return [] # check if camera token is in prefix. If so, and we have list of # renderable cameras, generate render product for each and every # of them. cameras = [ self.sanitize_camera_name(c) for c in self.get_renderable_cameras() ] default_ext = self._get_attr("defaultRenderGlobals.imfPluginKey") colorspace = self._get_colorspace( "defaultArnoldDriver.colorManagement" ) beauty_products = [ RenderProduct( productName="beauty", ext=default_ext, driver="defaultArnoldDriver", camera=camera, colorspace=colorspace ) for camera in cameras ] # AOVs > Legacy > Maya Render View > Mode aovs_enabled = bool( self._get_attr("defaultArnoldRenderOptions.aovMode") ) if not aovs_enabled: return beauty_products # Common > File Output > Merge AOVs or # We don't need to check for Merge AOVs due to overridden # `get_renderer_prefix()` behavior which forces has_renderpass_token = ( " " in self.layer_data.filePrefix.lower() ) if not has_renderpass_token: for product in beauty_products: product.multipart = True return beauty_products # AOVs are set to be rendered separately. We should expect # token in path. # handle aovs from references use_ref_aovs = self.render_instance.data.get( "useReferencedAovs", False) or False aovs = cmds.ls(type="aiAOV") if not use_ref_aovs: ref_aovs = cmds.ls(type="aiAOV", referencedNodes=True) aovs = list(set(aovs) - set(ref_aovs)) products = [] # Append the AOV products for aov in aovs: enabled = self._get_attr(aov, "enabled") if not enabled: continue # For now stick to the legacy output format. aov_products = self._get_aov_render_products(aov, cameras) products.extend(aov_products) if all(product.aov != "RGBA" for product in products): # Append default 'beauty' as this is arnolds default. # However, it is excluded whenever a RGBA pass is enabled. # For legibility add the beauty layer as first entry products += beauty_products # TODO: Output Denoising AOVs? return products class RenderProductsVray(ARenderProducts): """Expected files for V-Ray renderer. Notes: - "Disabled" animation incorrectly returns frames in filename - "Renumber Frames" is not supported Reference: vrayAddRenderElementImpl() in vrayCreateRenderElementsTab.mel """ # todo: detect whether rendering with V-Ray GPU + whether AOV is supported renderer = "vray" def get_multipart(self): multipart = False image_format = self._get_attr("vraySettings.imageFormatStr") if image_format == "exr (multichannel)": multipart = True return multipart def get_renderer_prefix(self): # type: () -> str """Get image prefix for V-Ray. This overrides :func:`ARenderProducts.get_renderer_prefix()` as we must add ` ` token manually. This is done only for non-multipart outputs, where ` ` token doesn't make sense. See also: :func:`ARenderProducts.get_renderer_prefix()` """ prefix = super(RenderProductsVray, self).get_renderer_prefix() if self.multipart: return prefix aov_separator = self._get_aov_separator() prefix = "{}{} ".format(prefix, aov_separator) return prefix def _get_aov_separator(self): # type: () -> str """Return the V-Ray AOV/Render Elements separator""" return self._get_attr( "vraySettings.fileNameRenderElementSeparator" ) def _get_layer_data(self): # type: () -> LayerMetadata """Override to get vray specific extension.""" layer_data = super(RenderProductsVray, self)._get_layer_data() default_ext = self._get_attr("vraySettings.imageFormatStr") if default_ext in ["exr (multichannel)", "exr (deep)"]: default_ext = "exr" layer_data.defaultExt = default_ext layer_data.padding = self._get_attr("vraySettings.fileNamePadding") layer_data.aov_separator = self._get_aov_separator() return layer_data def get_render_products(self): """Get all AOVs. See Also: :func:`ARenderProducts.get_render_products()` """ if not cmds.ls("vraySettings", type="VRaySettingsNode"): # this occurs when Render Setting windows was not opened yet. In # such case there are no VRay options created so query for AOVs # will fail. We terminate here as there are no AOVs specified then. # This state will most probably fail later on some Validator # anyway. return [] cameras = [ self.sanitize_camera_name(c) for c in self.get_renderable_cameras() ] image_format_str = self._get_attr("vraySettings.imageFormatStr") default_ext = image_format_str if default_ext in {"exr (multichannel)", "exr (deep)"}: default_ext = "exr" colorspace = lib.get_color_management_output_transform() products = [] # add beauty as default when not disabled dont_save_rgb = self._get_attr("vraySettings.dontSaveRgbChannel") if not dont_save_rgb: for camera in cameras: products.append( RenderProduct( productName="", ext=default_ext, camera=camera, colorspace=colorspace, multipart=self.multipart ) ) # separate alpha file separate_alpha = self._get_attr("vraySettings.separateAlpha") if separate_alpha: for camera in cameras: products.append( RenderProduct( productName="Alpha", ext=default_ext, camera=camera, colorspace=colorspace, multipart=self.multipart ) ) if self.multipart: # AOVs are merged in m-channel file, only main layer is rendered return products # handle aovs from references use_ref_aovs = self.render_instance.data.get( "useReferencedAovs", False) or False # this will have list of all aovs no matter if they are coming from # reference or not. aov_types = ["VRayRenderElement", "VRayRenderElementSet"] aovs = cmds.ls(type=aov_types) if not use_ref_aovs: ref_aovs = cmds.ls(type=aov_types, referencedNodes=True) or [] aovs = list(set(aovs) - set(ref_aovs)) for aov in aovs: enabled = self._get_attr(aov, "enabled") if not enabled: continue class_type = self._get_attr(aov + ".vrayClassType") if class_type == "LightMixElement": # Special case which doesn't define a name by itself but # instead seems to output multiple Render Products, # specifically "Self_Illumination" and "Environment" product_names = ["Self_Illumination", "Environment"] for camera in cameras: for name in product_names: product = RenderProduct(productName=name, ext=default_ext, aov=aov, camera=camera, colorspace=colorspace) products.append(product) # Continue as we've processed this special case AOV continue aov_name = self._get_vray_aov_name(aov) for camera in cameras: product = RenderProduct( productName=aov_name, ext=default_ext, aov=aov, camera=camera, colorspace=colorspace ) products.append(product) return products def _get_vray_aov_attr(self, node, prefix): """Get value for attribute that starts with key in name V-Ray AOVs have attribute names that include the type of AOV in the attribute name, for example: - vray_filename_rawdiffuse - vray_filename_velocity - vray_name_gi - vray_explicit_name_extratex To simplify querying the "vray_filename" or "vray_name" attributes we just find the first attribute that has that particular "{prefix}_" in the attribute name. Args: node (str): AOV node name prefix (str): Prefix of the attribute name. Returns: Value of the attribute if it exists, else None """ attrs = cmds.listAttr(node, string="{}_*".format(prefix)) if not attrs: return None assert len(attrs) == 1, "Found more than one attribute: %s" % attrs attr = attrs[0] return self._get_attr(node, attr) def _get_vray_aov_name(self, node): """Get AOVs name from Vray. Args: node (str): aov node name. Returns: str: aov name. """ vray_explicit_name = self._get_vray_aov_attr(node, "vray_explicit_name") vray_filename = self._get_vray_aov_attr(node, "vray_filename") vray_name = self._get_vray_aov_attr(node, "vray_name") final_name = vray_explicit_name or vray_filename or vray_name or None class_type = self._get_attr(node, "vrayClassType") if not vray_explicit_name: # Explicit name takes precedence and overrides completely # otherwise add the connected node names to the special cases # Any namespace colon ':' gets replaced to underscore '_' # so we sanitize using `sanitize_camera_name` def _get_source_name(node, attr): """Return sanitized name of input connection to attribute""" plug = "{}.{}".format(node, attr) connections = cmds.listConnections(plug, source=True, destination=False) if connections: return self.sanitize_camera_name(connections[0]) if class_type == "MaterialSelectElement": # Name suffix is based on the connected material or set attrs = [ "vray_mtllist_mtlselect", "vray_mtl_mtlselect" ] for attribute in attrs: name = _get_source_name(node, attribute) if name: final_name += '_{}'.format(name) break else: log.warning("Material Select Element has no " "selected materials: %s", node) elif class_type == "ExtraTexElement": # Name suffix is based on the connected textures extratex_type = self._get_attr(node, "vray_type_extratex") attr = { 0: "vray_texture_extratex", 1: "vray_float_texture_extratex", 2: "vray_int_texture_extratex", }.get(extratex_type) name = _get_source_name(node, attr) if name: final_name += '_{}'.format(name) else: log.warning("Extratex Element has no incoming texture") assert final_name, "Output filename not defined for AOV: %s" % node return final_name class RenderProductsRedshift(ARenderProducts): """Expected files for Redshift renderer. Notes: - `get_files()` only supports rendering with frames, like "animation" Attributes: unmerged_aovs (list): Name of aovs that are not merged into resulting exr and we need them specified in Render Products output. """ renderer = "redshift" unmerged_aovs = {"Cryptomatte"} def get_files(self, product): # When outputting AOVs we need to replace Redshift specific AOV tokens # with Maya render tokens for generating file sequences. We validate to # a specific AOV fileprefix so we only need to account for one # replacement. if not product.multipart and product.driver: file_prefix = self._get_attr(product.driver + ".filePrefix") self.layer_data.filePrefix = file_prefix.replace( " / ", " / / " ) return super(RenderProductsRedshift, self).get_files(product) def get_multipart(self): # For Redshift we don't directly return upon forcing multilayer # due to some AOVs still being written into separate files, # like Cryptomatte. # AOVs are merged in multi-channel file multipart = False force_layer = bool( self._get_attr("redshiftOptions.exrForceMultilayer") ) if force_layer: multipart = True return multipart def get_renderer_prefix(self): """Get image prefix for Redshift. This overrides :func:`ARenderProducts.get_renderer_prefix()` as we must add ` ` token manually. This is done only for non-multipart outputs, where ` ` token doesn't make sense. See also: :func:`ARenderProducts.get_renderer_prefix()` """ prefix = super(RenderProductsRedshift, self).get_renderer_prefix() if self.multipart: return prefix separator = self.extract_separator(prefix) prefix = "{}{} ".format(prefix, separator or "_") return prefix def get_render_products(self): """Get all AOVs. See Also: :func:`ARenderProducts.get_render_products()` """ if not cmds.ls("redshiftOptions", type="RedshiftOptions"): # this occurs when Render Setting windows was not opened yet. In # such case there are no Redshift options created so query for AOVs # will fail. We terminate here as there are no AOVs specified then. # This state will most probably fail later on some Validator # anyway. return [] cameras = [ self.sanitize_camera_name(c) for c in self.get_renderable_cameras() ] # Get Redshift Extension from image format image_format = self._get_attr("redshiftOptions.imageFormat") # integer ext = mel.eval("redshiftGetImageExtension(%i)" % image_format) use_ref_aovs = self.render_instance.data.get( "useReferencedAovs", False) or False aovs = cmds.ls(type="RedshiftAOV") if not use_ref_aovs: ref_aovs = cmds.ls(type="RedshiftAOV", referencedNodes=True) aovs = list(set(aovs) - set(ref_aovs)) products = [] light_groups_enabled = False has_beauty_aov = False colorspace = lib.get_color_management_output_transform() for aov in aovs: enabled = self._get_attr(aov, "enabled") if not enabled: continue aov_type = self._get_attr(aov, "aovType") if self.multipart and aov_type not in self.unmerged_aovs: continue # Any AOVs that still get processed, like Cryptomatte # by themselves are not multipart files. # Redshift skips rendering of masterlayer without AOV suffix # when a Beauty AOV is rendered. It overrides the main layer. if aov_type == "Beauty": has_beauty_aov = True aov_name = self._get_attr(aov, "name") # Support light Groups light_groups = [] if self._get_attr(aov, "supportsLightGroups"): all_light_groups = self._get_attr(aov, "allLightGroups") if all_light_groups: # All light groups is enabled light_groups = self._get_redshift_light_groups() else: value = self._get_attr(aov, "lightGroupList") # note: string value can return None when never set if value: selected_light_groups = value.strip().split() light_groups = selected_light_groups for light_group in light_groups: aov_light_group_name = "{}_{}".format(aov_name, light_group) for camera in cameras: product = RenderProduct( productName=aov_light_group_name, aov=aov_name, ext=ext, multipart=False, camera=camera, driver=aov, colorspace=colorspace) products.append(product) if light_groups: light_groups_enabled = True # Redshift AOV Light Select always renders the global AOV # even when light groups are present so we don't need to # exclude it when light groups are active for camera in cameras: product = RenderProduct(productName=aov_name, aov=aov_name, ext=ext, multipart=False, camera=camera, driver=aov, colorspace=colorspace) products.append(product) # When a Beauty AOV is added manually, it will be rendered as # 'Beauty_other' in file name and "standard" beauty will have # 'Beauty' in its name. When disabled, standard output will be # without `Beauty`. Except when using light groups. if light_groups_enabled: return products beauty_name = "BeautyAux" if has_beauty_aov else "" for camera in cameras: products.insert(0, RenderProduct(productName=beauty_name, ext=ext, multipart=self.multipart, camera=camera, colorspace=colorspace)) return products @staticmethod def _get_redshift_light_groups(): return sorted(mel.eval("redshiftAllAovLightGroups")) class RenderProductsRenderman(ARenderProducts): """Expected files for Renderman renderer. Warning: This is very rudimentary and needs more love and testing. """ renderer = "renderman" unmerged_aovs = {"PxrCryptomatte"} def get_multipart(self): # Implemented as display specific in "get_render_products". return False def get_render_products(self): """Get all AOVs. See Also: :func:`ARenderProducts.get_render_products()` """ from rfm2.api.displays import get_displays # noqa colorspace = lib.get_color_management_output_transform() cameras = [ self.sanitize_camera_name(c) for c in self.get_renderable_cameras() ] if not cameras: cameras = [ self.sanitize_camera_name( self.get_renderable_cameras()[0]) ] products = [] # NOTE: This is guessing extensions from renderman display types. # Some of them are just framebuffers, d_texture format can be # set in display setting. We set those now to None, but it # should be handled more gracefully. display_types = { "d_deepexr": "exr", "d_it": None, "d_null": None, "d_openexr": "exr", "d_png": "png", "d_pointcloud": "ptc", "d_targa": "tga", "d_texture": None, "d_tiff": "tif" } displays = get_displays(override_dst="render")["displays"] for name, display in displays.items(): enabled = display["params"]["enable"]["value"] if not enabled: continue # Skip display types not producing any file output. # Is there a better way to do it? if not display_types.get(display["driverNode"]["type"]): continue has_cryptomatte = cmds.ls(type=self.unmerged_aovs) matte_enabled = False if has_cryptomatte: for cryptomatte in has_cryptomatte: cryptomatte_aov = cryptomatte matte_name = "cryptomatte" rman_globals = cmds.listConnections(cryptomatte + ".message") if rman_globals: matte_enabled = True aov_name = name if aov_name == "rmanDefaultDisplay": aov_name = "beauty" extensions = display_types.get( display["driverNode"]["type"], "exr") for camera in cameras: # Create render product and set it as multipart only on # display types supporting it. In all other cases, Renderman # will create separate output per channel. if display["driverNode"]["type"] in ["d_openexr", "d_deepexr", "d_tiff"]: # noqa product = RenderProduct( productName=aov_name, ext=extensions, camera=camera, multipart=True, colorspace=colorspace ) if has_cryptomatte and matte_enabled: cryptomatte = RenderProduct( productName=matte_name, aov=cryptomatte_aov, ext=extensions, camera=camera, multipart=True, colorspace=colorspace ) else: # this code should handle the case where no multipart # capable format is selected. But since it involves # shady logic to determine what channel become what # lets not do that as all productions will use exr anyway. """ for channel in display['params']['displayChannels']['value']: # noqa product = RenderProduct( productName="{}_{}".format(aov_name, channel), ext=extensions, camera=camera, multipart=False ) """ raise UnsupportedImageFormatException( "Only exr, deep exr and tiff formats are supported.") products.append(product) if has_cryptomatte and matte_enabled: products.append(cryptomatte) return products def get_files(self, product): """Get expected files. """ files = super(RenderProductsRenderman, self).get_files(product) layer_data = self.layer_data new_files = [] resolved_image_dir = re.sub(" ", layer_data.sceneName, RENDERMAN_IMAGE_DIR, flags=re.IGNORECASE) # noqa: E501 resolved_image_dir = re.sub(" ", layer_data.layerName, resolved_image_dir, flags=re.IGNORECASE) # noqa: E501 for file in files: new_file = "{}/{}".format(resolved_image_dir, file) new_files.append(new_file) return new_files class RenderProductsMayaHardware(ARenderProducts): """Expected files for MayaHardware renderer.""" renderer = "mayahardware2" extensions = [ {"label": "JPEG", "index": 8, "extension": "jpg"}, {"label": "PNG", "index": 32, "extension": "png"}, {"label": "EXR(exr)", "index": 40, "extension": "exr"} ] def get_multipart(self): # MayaHardware does not support multipart EXRs. return False def _get_extension(self, value): result = None if isinstance(value, int): extensions = { extension["index"]: extension["extension"] for extension in self.extensions } try: result = extensions[value] except KeyError: raise NotImplementedError( "Could not find extension for {}".format(value) ) if isinstance(value, six.string_types): extensions = { extension["label"]: extension["extension"] for extension in self.extensions } try: result = extensions[value] except KeyError: raise NotImplementedError( "Could not find extension for {}".format(value) ) if not result: raise NotImplementedError( "Could not find extension for {}".format(value) ) return result def get_render_products(self): """Get all AOVs. See Also: :func:`ARenderProducts.get_render_products()` """ ext = self._get_extension( self._get_attr("defaultRenderGlobals.imageFormat") ) products = [] for cam in self.get_renderable_cameras(): product = RenderProduct( productName="beauty", ext=ext, camera=cam, colorspace=lib.get_color_management_output_transform() ) products.append(product) return products class AOVError(Exception): """Custom exception for determining AOVs.""" class UnsupportedRendererException(Exception): """Custom exception. Raised when requesting data from unsupported renderer. """ class UnsupportedImageFormatException(Exception): """Custom exception to report unsupported output image format.""" ================================================ FILE: openpype/hosts/maya/api/lib_rendersettings.py ================================================ # -*- coding: utf-8 -*- """Class for handling Render Settings.""" import six import sys from openpype.lib import Logger from openpype.settings import get_project_settings from openpype.pipeline import CreatorError, get_current_project_name from openpype.pipeline.context_tools import get_current_project_asset from openpype.hosts.maya.api.lib import reset_frame_range class RenderSettings(object): _image_prefix_nodes = { 'vray': 'vraySettings.fileNamePrefix', 'arnold': 'defaultRenderGlobals.imageFilePrefix', 'renderman': 'rmanGlobals.imageFileFormat', 'redshift': 'defaultRenderGlobals.imageFilePrefix', 'mayahardware2': 'defaultRenderGlobals.imageFilePrefix' } _aov_chars = { "dot": ".", "dash": "-", "underscore": "_" } log = Logger.get_logger("RenderSettings") @classmethod def get_image_prefix_attr(cls, renderer): return cls._image_prefix_nodes[renderer] @staticmethod def get_padding_attr(renderer): """Return attribute for renderer that defines frame padding amount""" if renderer == "vray": return "vraySettings.fileNamePadding" else: return "defaultRenderGlobals.extensionPadding" def __init__(self, project_settings=None): if not project_settings: project_settings = get_project_settings( get_current_project_name() ) render_settings = project_settings["maya"]["RenderSettings"] image_prefixes = { "vray": render_settings["vray_renderer"]["image_prefix"], "arnold": render_settings["arnold_renderer"]["image_prefix"], "renderman": render_settings["renderman_renderer"]["image_prefix"], "redshift": render_settings["redshift_renderer"]["image_prefix"] } # TODO probably should be stored to more explicit attribute # Renderman only renderman_settings = render_settings["renderman_renderer"] _image_dir = { "renderman": renderman_settings["image_dir"], "cryptomatte": renderman_settings["cryptomatte_dir"], "imageDisplay": renderman_settings["imageDisplay_dir"], "watermark": renderman_settings["watermark_dir"] } self._image_prefixes = image_prefixes self._image_dir = _image_dir self._project_settings = project_settings def set_default_renderer_settings(self, renderer=None): """Set basic settings based on renderer.""" # Not all hosts can import this module. from maya import cmds # noqa: F401 import maya.mel as mel # noqa: F401 if not renderer: renderer = cmds.getAttr( 'defaultRenderGlobals.currentRenderer').lower() asset_doc = get_current_project_asset() # project_settings/maya/create/CreateRender/aov_separator try: aov_separator = self._aov_chars[( self._project_settings["maya"] ["RenderSettings"] ["aov_separator"] )] except KeyError: aov_separator = "_" reset_frame = self._project_settings["maya"]["RenderSettings"]["reset_current_frame"] # noqa if reset_frame: start_frame = cmds.getAttr("defaultRenderGlobals.startFrame") cmds.currentTime(start_frame, edit=True) if renderer in self._image_prefix_nodes: prefix = self._image_prefixes[renderer] prefix = prefix.replace("{aov_separator}", aov_separator) cmds.setAttr(self._image_prefix_nodes[renderer], prefix, type="string") # noqa else: print("{0} isn't a supported renderer to autoset settings.".format(renderer)) # noqa # TODO: handle not having res values in the doc width = asset_doc["data"].get("resolutionWidth") height = asset_doc["data"].get("resolutionHeight") if renderer == "arnold": # set renderer settings for Arnold from project settings self._set_arnold_settings(width, height) if renderer == "vray": self._set_vray_settings(aov_separator, width, height) if renderer == "redshift": self._set_redshift_settings(width, height) mel.eval("redshiftUpdateActiveAovList") if renderer == "renderman": image_dir = self._image_dir["renderman"] cmds.setAttr("rmanGlobals.imageOutputDir", image_dir, type="string") self._set_renderman_settings(width, height, aov_separator) def _set_arnold_settings(self, width, height): """Sets settings for Arnold.""" from mtoa.core import createOptions # noqa from mtoa.aovs import AOVInterface # noqa # Not all hosts can import this module. from maya import cmds # noqa: F401 import maya.mel as mel # noqa: F401 createOptions() render_settings = self._project_settings["maya"]["RenderSettings"] arnold_render_presets = render_settings["arnold_renderer"] # noqa # Force resetting settings and AOV list to avoid having to deal with # AOV checking logic, for now. # This is a work around because the standard # function to revert render settings does not reset AOVs list in MtoA # Fetch current aovs in case there's any. current_aovs = AOVInterface().getAOVs() remove_aovs = render_settings["remove_aovs"] if remove_aovs: # Remove fetched AOVs AOVInterface().removeAOVs(current_aovs) mel.eval("unifiedRenderGlobalsRevertToDefault") img_ext = arnold_render_presets["image_format"] img_prefix = arnold_render_presets["image_prefix"] aovs = arnold_render_presets["aov_list"] img_tiled = arnold_render_presets["tiled"] multi_exr = arnold_render_presets["multilayer_exr"] additional_options = arnold_render_presets["additional_options"] for aov in aovs: if aov in current_aovs and not remove_aovs: continue AOVInterface('defaultArnoldRenderOptions').addAOV(aov) cmds.setAttr("defaultResolution.width", width) cmds.setAttr("defaultResolution.height", height) self._set_global_output_settings() cmds.setAttr( "defaultRenderGlobals.imageFilePrefix", img_prefix, type="string") cmds.setAttr( "defaultArnoldDriver.ai_translator", img_ext, type="string") cmds.setAttr( "defaultArnoldDriver.exrTiled", img_tiled) cmds.setAttr( "defaultArnoldDriver.mergeAOVs", multi_exr) self._additional_attribs_setter(additional_options) reset_frame_range(playback=False, fps=False, render=True) def _set_redshift_settings(self, width, height): """Sets settings for Redshift.""" # Not all hosts can import this module. from maya import cmds # noqa: F401 import maya.mel as mel # noqa: F401 render_settings = self._project_settings["maya"]["RenderSettings"] redshift_render_presets = render_settings["redshift_renderer"] remove_aovs = render_settings["remove_aovs"] all_rs_aovs = cmds.ls(type='RedshiftAOV') if remove_aovs: for aov in all_rs_aovs: enabled = cmds.getAttr("{}.enabled".format(aov)) if enabled: cmds.delete(aov) redshift_aovs = redshift_render_presets["aov_list"] # list all the aovs all_rs_aovs = cmds.ls(type='RedshiftAOV') for rs_aov in redshift_aovs: rs_layername = "rsAov_{}".format(rs_aov.replace(" ", "")) if rs_layername in all_rs_aovs: continue cmds.rsCreateAov(type=rs_aov) # update the AOV list mel.eval("redshiftUpdateActiveAovList") rs_p_engine = redshift_render_presets["primary_gi_engine"] rs_s_engine = redshift_render_presets["secondary_gi_engine"] if int(rs_p_engine) or int(rs_s_engine) != 0: cmds.setAttr("redshiftOptions.GIEnabled", 1) if int(rs_p_engine) == 0: # reset the primary GI Engine as default cmds.setAttr("redshiftOptions.primaryGIEngine", 4) if int(rs_s_engine) == 0: # reset the secondary GI Engine as default cmds.setAttr("redshiftOptions.secondaryGIEngine", 2) else: cmds.setAttr("redshiftOptions.GIEnabled", 0) cmds.setAttr("redshiftOptions.primaryGIEngine", int(rs_p_engine)) cmds.setAttr("redshiftOptions.secondaryGIEngine", int(rs_s_engine)) additional_options = redshift_render_presets["additional_options"] ext = redshift_render_presets["image_format"] img_exts = ["iff", "exr", "tif", "png", "tga", "jpg"] img_ext = img_exts.index(ext) self._set_global_output_settings() cmds.setAttr("redshiftOptions.imageFormat", img_ext) cmds.setAttr("defaultResolution.width", width) cmds.setAttr("defaultResolution.height", height) self._additional_attribs_setter(additional_options) def _set_renderman_settings(self, width, height, aov_separator): """Sets settings for Renderman""" # Not all hosts can import this module. from maya import cmds # noqa: F401 import maya.mel as mel # noqa: F401 rman_render_presets = ( self._project_settings ["maya"] ["RenderSettings"] ["renderman_renderer"] ) display_filters = rman_render_presets["display_filters"] d_filters_number = len(display_filters) for i in range(d_filters_number): d_node = cmds.ls(typ=display_filters[i]) if len(d_node) > 0: filter_nodes = d_node[0] else: filter_nodes = cmds.createNode(display_filters[i]) cmds.connectAttr(filter_nodes + ".message", "rmanGlobals.displayFilters[%i]" % i, force=True) if filter_nodes.startswith("PxrImageDisplayFilter"): imageDisplay_dir = self._image_dir["imageDisplay"] imageDisplay_dir = imageDisplay_dir.replace("{aov_separator}", aov_separator) cmds.setAttr(filter_nodes + ".filename", imageDisplay_dir, type="string") sample_filters = rman_render_presets["sample_filters"] s_filters_number = len(sample_filters) for n in range(s_filters_number): s_node = cmds.ls(typ=sample_filters[n]) if len(s_node) > 0: filter_nodes = s_node[0] else: filter_nodes = cmds.createNode(sample_filters[n]) cmds.connectAttr(filter_nodes + ".message", "rmanGlobals.sampleFilters[%i]" % n, force=True) if filter_nodes.startswith("PxrCryptomatte"): matte_dir = self._image_dir["cryptomatte"] matte_dir = matte_dir.replace("{aov_separator}", aov_separator) cmds.setAttr(filter_nodes + ".filename", matte_dir, type="string") elif filter_nodes.startswith("PxrWatermarkFilter"): watermark_dir = self._image_dir["watermark"] watermark_dir = watermark_dir.replace("{aov_separator}", aov_separator) cmds.setAttr(filter_nodes + ".filename", watermark_dir, type="string") additional_options = rman_render_presets["additional_options"] self._set_global_output_settings() cmds.setAttr("defaultResolution.width", width) cmds.setAttr("defaultResolution.height", height) self._additional_attribs_setter(additional_options) def _set_vray_settings(self, aov_separator, width, height): # type: (str, int, int) -> None """Sets important settings for Vray.""" # Not all hosts can import this module. from maya import cmds # noqa: F401 import maya.mel as mel # noqa: F401 settings = cmds.ls(type="VRaySettingsNode") node = settings[0] if settings else cmds.createNode("VRaySettingsNode") render_settings = self._project_settings["maya"]["RenderSettings"] vray_render_presets = render_settings["vray_renderer"] # vrayRenderElement remove_aovs = render_settings["remove_aovs"] all_vray_aovs = cmds.ls(type='VRayRenderElement') lightSelect_aovs = cmds.ls(type='VRayRenderElementSet') if remove_aovs: for aov in all_vray_aovs: # remove all aovs except LightSelect enabled = cmds.getAttr("{}.enabled".format(aov)) if enabled: cmds.delete(aov) # remove LightSelect for light_aovs in lightSelect_aovs: light_enabled = cmds.getAttr("{}.enabled".format(light_aovs)) if light_enabled: cmds.delete(lightSelect_aovs) vray_aovs = vray_render_presets["aov_list"] for renderlayer in vray_aovs: renderElement = "vrayAddRenderElement {}".format(renderlayer) RE_name = mel.eval(renderElement) # if there is more than one same render element if RE_name.endswith("1"): cmds.delete(RE_name) # Set aov separator # First we need to explicitly set the UI items in Render Settings # because that is also what V-Ray updates to when that Render Settings # UI did initialize before and refreshes again. MENU = "vrayRenderElementSeparator" if cmds.optionMenuGrp(MENU, query=True, exists=True): items = cmds.optionMenuGrp(MENU, query=True, ill=True) separators = [cmds.menuItem(i, query=True, label=True) for i in items] # noqa: E501 try: sep_idx = separators.index(aov_separator) except ValueError: six.reraise( CreatorError, CreatorError( "AOV character {} not in {}".format( aov_separator, separators)), sys.exc_info()[2]) cmds.optionMenuGrp(MENU, edit=True, select=sep_idx + 1) # Set the render element attribute as string. This is also what V-Ray # sets whenever the `vrayRenderElementSeparator` menu items switch cmds.setAttr( "{}.fileNameRenderElementSeparator".format(node), aov_separator, type="string" ) # Set render file format to exr ext = vray_render_presets["image_format"] cmds.setAttr("{}.imageFormatStr".format(node), ext, type="string") # animType cmds.setAttr("{}.animType".format(node), 1) # resolution cmds.setAttr("{}.width".format(node), width) cmds.setAttr("{}.height".format(node), height) additional_options = vray_render_presets["additional_options"] self._additional_attribs_setter(additional_options) @staticmethod def _set_global_output_settings(): # Not all hosts can import this module. from maya import cmds # noqa: F401 import maya.mel as mel # noqa: F401 # enable animation cmds.setAttr("defaultRenderGlobals.outFormatControl", 0) cmds.setAttr("defaultRenderGlobals.animation", 1) cmds.setAttr("defaultRenderGlobals.putFrameBeforeExt", 1) cmds.setAttr("defaultRenderGlobals.extensionPadding", 4) def _additional_attribs_setter(self, additional_attribs): # Not all hosts can import this module. from maya import cmds # noqa: F401 import maya.mel as mel # noqa: F401 for item in additional_attribs: attribute, value = item attribute = str(attribute) # ensure str conversion from settings attribute_type = cmds.getAttr(attribute, type=True) if attribute_type in {"long", "bool"}: cmds.setAttr(attribute, int(value)) elif attribute_type == "string": cmds.setAttr(attribute, str(value), type="string") elif attribute_type in {"double", "doubleAngle", "doubleLinear"}: cmds.setAttr(attribute, float(value)) else: self.log.error( "Attribute {attribute} can not be set due to unsupported " "type: {attribute_type}".format( attribute=attribute, attribute_type=attribute_type) ) ================================================ FILE: openpype/hosts/maya/api/lib_rendersetup.py ================================================ # -*- coding: utf-8 -*- """Code to get attributes from render layer without switching to it. https://github.com/Colorbleed/colorbleed-config/blob/acre/colorbleed/maya/lib_rendersetup.py Credits: Roy Nieterau (BigRoy) / Colorbleed Modified for use in OpenPype """ from maya import cmds import maya.api.OpenMaya as om import logging import maya.app.renderSetup.model.utils as utils from maya.app.renderSetup.model import renderSetup from maya.app.renderSetup.model.override import ( AbsOverride, RelOverride, UniqueOverride ) from openpype.hosts.maya.api.lib import get_attribute EXACT_MATCH = 0 PARENT_MATCH = 1 CLIENT_MATCH = 2 DEFAULT_RENDER_LAYER = "defaultRenderLayer" log = logging.getLogger(__name__) def get_rendersetup_layer(layer): """Return render setup layer name. This also converts names from legacy renderLayer node name to render setup name. Note: `defaultRenderLayer` is not a renderSetupLayer node but it is however the valid layer name for Render Setup - so we return that as is. Example: >>> for legacy_layer in cmds.ls(type="renderLayer"): >>> layer = get_rendersetup_layer(legacy_layer) Returns: str or None: Returns renderSetupLayer node name if `layer` is a valid layer name in legacy renderlayers or render setup layers. Returns None if the layer can't be found or Render Setup is currently disabled. """ if layer == DEFAULT_RENDER_LAYER: # defaultRenderLayer doesn't have a `renderSetupLayer` return layer if not cmds.mayaHasRenderSetup(): return None if not cmds.objExists(layer): return None if cmds.nodeType(layer) == "renderSetupLayer": return layer # By default Render Setup renames the legacy renderlayer # to `rs_ ` but lets not rely on that as the # layer node can be renamed manually connections = cmds.listConnections(layer + ".message", type="renderSetupLayer", exactType=True, source=False, destination=True, plugs=True) or [] return next((conn.split(".", 1)[0] for conn in connections if conn.endswith(".legacyRenderLayer")), None) def get_attr_in_layer(node_attr, layer): """Return attribute value in Render Setup layer. This will only work for attributes which can be retrieved with `maya.cmds.getAttr` and for which Relative and Absolute overrides are applicable. Examples: >>> get_attr_in_layer("defaultResolution.width", layer="layer1") >>> get_attr_in_layer("defaultRenderGlobals.startFrame", layer="layer") >>> get_attr_in_layer("transform.translate", layer="layer3") Args: attr (str): attribute name as 'node.attribute' layer (str): layer name Returns: object: attribute value in layer """ def _layer_needs_update(layer): """Return whether layer needs updating.""" # Use `getattr` as e.g. DEFAULT_RENDER_LAYER does not have # the attribute return getattr(layer, "needsMembershipUpdate", False) or \ getattr(layer, "needsApplyUpdate", False) def get_default_layer_value(node_attr_): """Return attribute value in `DEFAULT_RENDER_LAYER`.""" inputs = cmds.listConnections(node_attr_, source=True, destination=False, # We want to skip conversion nodes since # an override to `endFrame` could have # a `unitToTimeConversion` node # in-between skipConversionNodes=True, type="applyOverride") or [] if inputs: override = inputs[0] history_overrides = cmds.ls(cmds.listHistory(override, pruneDagObjects=True), type="applyOverride") node = history_overrides[-1] if history_overrides else override node_attr_ = node + ".original" return get_attribute(node_attr_, asString=True) layer = get_rendersetup_layer(layer) rs = renderSetup.instance() current_layer = rs.getVisibleRenderLayer() if current_layer.name() == layer: # Ensure layer is up-to-date if _layer_needs_update(current_layer): try: rs.switchToLayer(current_layer) except RuntimeError: # Some cases can cause errors on switching # the first time with Render Setup layers # e.g. different overrides to compounds # and its children plugs. So we just force # it another time. If it then still fails # we will let it error out. rs.switchToLayer(current_layer) return get_attribute(node_attr, asString=True) overrides = get_attr_overrides(node_attr, layer) default_layer_value = get_default_layer_value(node_attr) if not overrides: return default_layer_value value = default_layer_value for match, layer_override, index in overrides: if isinstance(layer_override, AbsOverride): # Absolute override value = get_attribute(layer_override.name() + ".attrValue") if match == EXACT_MATCH: # value = value pass elif match == PARENT_MATCH: value = value[index] elif match == CLIENT_MATCH: value[index] = value elif isinstance(layer_override, RelOverride): # Relative override # Value = Original * Multiply + Offset multiply = get_attribute(layer_override.name() + ".multiply") offset = get_attribute(layer_override.name() + ".offset") if match == EXACT_MATCH: value = value * multiply + offset elif match == PARENT_MATCH: value = value * multiply[index] + offset[index] elif match == CLIENT_MATCH: value[index] = value[index] * multiply + offset else: raise TypeError("Unsupported override: %s" % layer_override) return value def get_attr_overrides(node_attr, layer, skip_disabled=True, skip_local_render=True, stop_at_absolute_override=True): """Return all Overrides applicable to the attribute. Overrides are returned as a 3-tuple: (Match, Override, Index) Match: This is any of EXACT_MATCH, PARENT_MATCH, CLIENT_MATCH and defines whether the override is exactly on the plug, on the parent or on a child plug. Override: This is the RenderSetup Override instance. Index: This is the Plug index under the parent or for the child that matches. The EXACT_MATCH index will always be None. For PARENT_MATCH the index is which index the plug is under the parent plug. For CLIENT_MATCH the index is which child index matches the plug. Args: node_attr (str): attribute name as 'node.attribute' layer (str): layer name skip_disabled (bool): exclude disabled overrides skip_local_render (bool): exclude overrides marked as local render. stop_at_absolute_override: exclude overrides prior to the last absolute override as they have no influence on the resulting value. Returns: list: Ordered Overrides in order of strength """ def get_mplug_children(plug): """Return children MPlugs of compound `MPlug`.""" children = [] if plug.isCompound: for i in range(plug.numChildren()): children.append(plug.child(i)) return children def get_mplug_names(mplug): """Return long and short name of `MPlug`.""" long_name = mplug.partialName(useLongNames=True) short_name = mplug.partialName(useLongNames=False) return {long_name, short_name} def iter_override_targets(override): try: for target in override._targets(): yield target except AssertionError: # Workaround: There is a bug where the private `_targets()` method # fails on some attribute plugs. For example overrides # to the defaultRenderGlobals.endFrame # (Tested in Maya 2020.2) log.debug("Workaround for %s" % override) from maya.app.renderSetup.common.utils import findPlug attr = override.attributeName() if isinstance(override, UniqueOverride): node = override.targetNodeName() yield findPlug(node, attr) else: nodes = override.parent().selector().nodes() for node in nodes: if cmds.attributeQuery(attr, node=node, exists=True): yield findPlug(node, attr) # Get the MPlug for the node.attr sel = om.MSelectionList() sel.add(node_attr) plug = sel.getPlug(0) layer = get_rendersetup_layer(layer) if layer == DEFAULT_RENDER_LAYER: # DEFAULT_RENDER_LAYER will never have overrides # since it's the default layer return [] rs_layer = renderSetup.instance().getRenderLayer(layer) if rs_layer is None: # Renderlayer does not exist return # Get any parent or children plugs as we also # want to include them in the attribute match # for overrides parent = plug.parent() if plug.isChild else None parent_index = None if parent: parent_index = get_mplug_children(parent).index(plug) children = get_mplug_children(plug) # Create lookup for the attribute by both long # and short names attr_names = get_mplug_names(plug) for child in children: attr_names.update(get_mplug_names(child)) if parent: attr_names.update(get_mplug_names(parent)) # Get all overrides of the layer # And find those that are relevant to the attribute plug_overrides = [] # Iterate over the overrides in reverse so we get the last # overrides first and can "break" whenever an absolute # override is reached layer_overrides = list(utils.getOverridesRecursive(rs_layer)) for layer_override in reversed(layer_overrides): if skip_disabled and not layer_override.isEnabled(): # Ignore disabled overrides continue if skip_local_render and layer_override.isLocalRender(): continue # The targets list can be very large so we'll do # a quick filter by attribute name to detect whether # it matches the attribute name, or its parent or child if layer_override.attributeName() not in attr_names: continue override_match = None for override_plug in iter_override_targets(layer_override): override_match = None if plug == override_plug: override_match = (EXACT_MATCH, layer_override, None) elif parent and override_plug == parent: override_match = (PARENT_MATCH, layer_override, parent_index) elif children and override_plug in children: child_index = children.index(override_plug) override_match = (CLIENT_MATCH, layer_override, child_index) if override_match: plug_overrides.append(override_match) break if ( override_match and stop_at_absolute_override and isinstance(layer_override, AbsOverride) and # When the override is only on a child plug then it doesn't # override the entire value so we not stop at this override not override_match[0] == CLIENT_MATCH ): # If override is absolute override, then BREAK out # of parent loop we don't need to look any further as # this is the absolute override break return reversed(plug_overrides) def get_shader_in_layer(node, layer): """Return the assigned shader in a renderlayer without switching layers. This has been developed and tested for Legacy Renderlayers and *not* for Render Setup. Note: This will also return the shader for any face assignments, however it will *not* return the components they are assigned to. This could be implemented, but since Maya's renderlayers are famous for breaking with face assignments there has been no need for this function to support that. Returns: list: The list of assigned shaders in the given layer. """ def _get_connected_shader(plug): """Return current shader""" return cmds.listConnections(plug, source=False, destination=True, plugs=False, connections=False, type="shadingEngine") or [] # We check the instObjGroups (shader connection) for layer overrides. plug = node + ".instObjGroups" # Ignore complex query if we're in the layer anyway (optimization) current_layer = cmds.editRenderLayerGlobals(query=True, currentRenderLayer=True) if layer == current_layer: return _get_connected_shader(plug) connections = cmds.listConnections(plug, plugs=True, source=False, destination=True, type="renderLayer") or [] connections = filter(lambda x: x.endswith(".outPlug"), connections) if not connections: # If no overrides anywhere on the shader, just get the current shader return _get_connected_shader(plug) def _get_override(connections, layer): """Return the overridden connection for that layer in connections""" # If there's an override on that layer, return that. for connection in connections: if (connection.startswith(layer + ".outAdjustments") and connection.endswith(".outPlug")): # This is a shader override on that layer so get the shader # connected to .outValue of the .outAdjustment[i] out_adjustment = connection.rsplit(".", 1)[0] connection_attr = out_adjustment + ".outValue" override = cmds.listConnections(connection_attr) or [] return override override_shader = _get_override(connections, layer) if override_shader is not None: return override_shader else: # Get the override for "defaultRenderLayer" (=masterLayer) return _get_override(connections, layer="defaultRenderLayer") ================================================ FILE: openpype/hosts/maya/api/menu.py ================================================ import os import logging from functools import partial from qtpy import QtWidgets, QtGui import maya.utils import maya.cmds as cmds from openpype.pipeline import ( get_current_asset_name, get_current_task_name ) from openpype.pipeline.workfile import BuildWorkfile from openpype.tools.utils import host_tools from openpype.hosts.maya.api import lib, lib_rendersettings from .lib import get_main_window, IS_HEADLESS from ..tools import show_look_assigner from .workfile_template_builder import ( create_placeholder, update_placeholder, build_workfile_template, update_workfile_template, ) log = logging.getLogger(__name__) MENU_NAME = "op_maya_menu" def _get_menu(menu_name=None): """Return the menu instance if it currently exists in Maya""" if menu_name is None: menu_name = MENU_NAME widgets = {w.objectName(): w for w in QtWidgets.QApplication.allWidgets()} return widgets.get(menu_name) def get_context_label(): return "{}, {}".format( get_current_asset_name(), get_current_task_name() ) def install(project_settings): if cmds.about(batch=True): log.info("Skipping openpype.menu initialization in batch mode..") return def add_menu(): pyblish_icon = host_tools.get_pyblish_icon() parent_widget = get_main_window() cmds.menu( MENU_NAME, label=os.environ.get("AVALON_LABEL") or "OpenPype", tearOff=True, parent="MayaWindow" ) # Create context menu cmds.menuItem( "currentContext", label=get_context_label(), parent=MENU_NAME, enable=False ) cmds.setParent("..", menu=True) cmds.menuItem(divider=True) cmds.menuItem( "Create...", command=lambda *args: host_tools.show_publisher( parent=parent_widget, tab="create" ) ) cmds.menuItem( "Load...", command=lambda *args: host_tools.show_loader( parent=parent_widget, use_context=True ) ) cmds.menuItem( "Publish...", command=lambda *args: host_tools.show_publisher( parent=parent_widget, tab="publish" ), image=pyblish_icon ) cmds.menuItem( "Manage...", command=lambda *args: host_tools.show_scene_inventory( parent=parent_widget ) ) cmds.menuItem( "Library...", command=lambda *args: host_tools.show_library_loader( parent=parent_widget ) ) cmds.menuItem(divider=True) cmds.menuItem( "Work Files...", command=lambda *args: host_tools.show_workfiles( parent=parent_widget ), ) cmds.menuItem( "Set Frame Range", command=lambda *args: lib.reset_frame_range() ) cmds.menuItem( "Set Resolution", command=lambda *args: lib.reset_scene_resolution() ) cmds.menuItem( "Set Colorspace", command=lambda *args: lib.set_colorspace(), ) cmds.menuItem( "Set Render Settings", command=lambda *args: lib_rendersettings.RenderSettings().set_default_renderer_settings() # noqa ) cmds.menuItem(divider=True, parent=MENU_NAME) cmds.menuItem( "Build First Workfile", parent=MENU_NAME, command=lambda *args: BuildWorkfile().process() ) cmds.menuItem( "Look assigner...", command=lambda *args: show_look_assigner( parent_widget ) ) cmds.menuItem( "Experimental tools...", command=lambda *args: host_tools.show_experimental_tools_dialog( parent_widget ) ) builder_menu = cmds.menuItem( "Template Builder", subMenu=True, tearOff=True, parent=MENU_NAME ) cmds.menuItem( "Create Placeholder", parent=builder_menu, command=create_placeholder ) cmds.menuItem( "Update Placeholder", parent=builder_menu, command=update_placeholder ) cmds.menuItem( "Build Workfile from template", parent=builder_menu, command=build_workfile_template ) cmds.menuItem( "Update Workfile from template", parent=builder_menu, command=update_workfile_template ) cmds.setParent(MENU_NAME, menu=True) def add_scripts_menu(project_settings): try: import scriptsmenu.launchformaya as launchformaya except ImportError: log.warning( "Skipping studio.menu install, because " "'scriptsmenu' module seems unavailable." ) return config = project_settings["maya"]["scriptsmenu"]["definition"] _menu = project_settings["maya"]["scriptsmenu"]["name"] if not config: log.warning("Skipping studio menu, no definition found.") return # run the launcher for Maya menu studio_menu = launchformaya.main( title=_menu.title(), objectName=_menu.title().lower().replace(" ", "_") ) # apply configuration studio_menu.build_from_configuration(studio_menu, config) # Allow time for uninstallation to finish. # We use Maya's executeDeferred instead of QTimer.singleShot # so that it only gets called after Maya UI has initialized too. # This is crucial with Maya 2020+ which initializes without UI # first as a QCoreApplication maya.utils.executeDeferred(add_menu) cmds.evalDeferred(partial(add_scripts_menu, project_settings), lowestPriority=True) def uninstall(): menu = _get_menu() if menu: log.info("Attempting to uninstall ...") try: menu.deleteLater() del menu except Exception as e: log.error(e) def popup(): """Pop-up the existing menu near the mouse cursor.""" menu = _get_menu() cursor = QtGui.QCursor() point = cursor.pos() menu.exec_(point) def update_menu_task_label(): """Update the task label in Avalon menu to current session""" if IS_HEADLESS: return object_name = "{}|currentContext".format(MENU_NAME) if not cmds.menuItem(object_name, query=True, exists=True): log.warning("Can't find menuItem: {}".format(object_name)) return label = get_context_label() cmds.menuItem(object_name, edit=True, label=label) ================================================ FILE: openpype/hosts/maya/api/pipeline.py ================================================ import json import base64 import os import errno import logging import contextlib import shutil from maya import utils, cmds, OpenMaya import maya.api.OpenMaya as om import pyblish.api from openpype.settings import get_project_settings from openpype.host import ( HostBase, IWorkfileHost, ILoadHost, IPublishHost, HostDirmap, ) from openpype.tools.utils import host_tools from openpype.tools.workfiles.lock_dialog import WorkfileLockDialog from openpype.lib import ( register_event_callback, emit_event ) from openpype.pipeline import ( legacy_io, get_current_project_name, register_loader_plugin_path, register_inventory_action_path, register_creator_plugin_path, deregister_loader_plugin_path, deregister_inventory_action_path, deregister_creator_plugin_path, AVALON_CONTAINER_ID, ) from openpype.pipeline.load import any_outdated_containers from openpype.pipeline.workfile.lock_workfile import ( create_workfile_lock, remove_workfile_lock, is_workfile_locked, is_workfile_lock_enabled ) from openpype.hosts.maya import MAYA_ROOT_DIR from openpype.hosts.maya.lib import create_workspace_mel from . import menu, lib from .workfile_template_builder import MayaPlaceholderLoadPlugin from .workio import ( open_file, save_file, file_extensions, has_unsaved_changes, work_root, current_file ) log = logging.getLogger("openpype.hosts.maya") PLUGINS_DIR = os.path.join(MAYA_ROOT_DIR, "plugins") PUBLISH_PATH = os.path.join(PLUGINS_DIR, "publish") LOAD_PATH = os.path.join(PLUGINS_DIR, "load") CREATE_PATH = os.path.join(PLUGINS_DIR, "create") INVENTORY_PATH = os.path.join(PLUGINS_DIR, "inventory") AVALON_CONTAINERS = ":AVALON_CONTAINERS" class MayaHost(HostBase, IWorkfileHost, ILoadHost, IPublishHost): name = "maya" def __init__(self): super(MayaHost, self).__init__() self._op_events = {} def install(self): project_name = get_current_project_name() project_settings = get_project_settings(project_name) # process path mapping dirmap_processor = MayaDirmap("maya", project_name, project_settings) dirmap_processor.process_dirmap() pyblish.api.register_plugin_path(PUBLISH_PATH) pyblish.api.register_host("mayabatch") pyblish.api.register_host("mayapy") pyblish.api.register_host("maya") register_loader_plugin_path(LOAD_PATH) register_creator_plugin_path(CREATE_PATH) register_inventory_action_path(INVENTORY_PATH) self.log.info(PUBLISH_PATH) self.log.info("Installing callbacks ... ") register_event_callback("init", on_init) _set_project() if lib.IS_HEADLESS: self.log.info(( "Running in headless mode, skipping Maya save/open/new" " callback installation.." )) return self._register_callbacks() menu.install(project_settings) register_event_callback("save", on_save) register_event_callback("open", on_open) register_event_callback("new", on_new) register_event_callback("before.save", on_before_save) register_event_callback("after.save", on_after_save) register_event_callback("before.close", on_before_close) register_event_callback("before.file.open", before_file_open) register_event_callback("taskChanged", on_task_changed) register_event_callback("workfile.open.before", before_workfile_open) register_event_callback("workfile.save.before", before_workfile_save) register_event_callback( "workfile.save.before", workfile_save_before_xgen ) register_event_callback("workfile.save.after", after_workfile_save) def open_workfile(self, filepath): return open_file(filepath) def save_workfile(self, filepath=None): return save_file(filepath) def work_root(self, session): return work_root(session) def get_current_workfile(self): return current_file() def workfile_has_unsaved_changes(self): return has_unsaved_changes() def get_workfile_extensions(self): return file_extensions() def get_containers(self): return ls() def get_workfile_build_placeholder_plugins(self): return [ MayaPlaceholderLoadPlugin ] @contextlib.contextmanager def maintained_selection(self): with lib.maintained_selection(): yield def get_context_data(self): data = cmds.fileInfo("OpenPypeContext", query=True) if not data: return {} data = data[0] # Maya seems to return a list decoded = base64.b64decode(data).decode("utf-8") return json.loads(decoded) def update_context_data(self, data, changes): json_str = json.dumps(data) encoded = base64.b64encode(json_str.encode("utf-8")) return cmds.fileInfo("OpenPypeContext", encoded) def _register_callbacks(self): for handler, event in self._op_events.copy().items(): if event is None: continue try: OpenMaya.MMessage.removeCallback(event) self._op_events[handler] = None except RuntimeError as exc: self.log.info(exc) self._op_events[_on_scene_save] = OpenMaya.MSceneMessage.addCallback( OpenMaya.MSceneMessage.kBeforeSave, _on_scene_save ) self._op_events[_after_scene_save] = ( OpenMaya.MSceneMessage.addCallback( OpenMaya.MSceneMessage.kAfterSave, _after_scene_save ) ) self._op_events[_before_scene_save] = ( OpenMaya.MSceneMessage.addCheckCallback( OpenMaya.MSceneMessage.kBeforeSaveCheck, _before_scene_save ) ) self._op_events[_on_scene_new] = OpenMaya.MSceneMessage.addCallback( OpenMaya.MSceneMessage.kAfterNew, _on_scene_new ) self._op_events[_on_maya_initialized] = ( OpenMaya.MSceneMessage.addCallback( OpenMaya.MSceneMessage.kMayaInitialized, _on_maya_initialized ) ) self._op_events[_on_scene_open] = ( OpenMaya.MSceneMessage.addCallback( OpenMaya.MSceneMessage.kAfterOpen, _on_scene_open ) ) self._op_events[_before_scene_open] = ( OpenMaya.MSceneMessage.addCallback( OpenMaya.MSceneMessage.kBeforeOpen, _before_scene_open ) ) self._op_events[_before_close_maya] = ( OpenMaya.MSceneMessage.addCallback( OpenMaya.MSceneMessage.kMayaExiting, _before_close_maya ) ) self.log.info("Installed event handler _on_scene_save..") self.log.info("Installed event handler _before_scene_save..") self.log.info("Installed event handler _on_after_save..") self.log.info("Installed event handler _on_scene_new..") self.log.info("Installed event handler _on_maya_initialized..") self.log.info("Installed event handler _on_scene_open..") self.log.info("Installed event handler _check_lock_file..") self.log.info("Installed event handler _before_close_maya..") def _set_project(): """Sets the maya project to the current Session's work directory. Returns: None """ workdir = legacy_io.Session["AVALON_WORKDIR"] try: os.makedirs(workdir) except OSError as e: # An already existing working directory is fine. if e.errno == errno.EEXIST: pass else: raise cmds.workspace(workdir, openWorkspace=True) def _on_maya_initialized(*args): emit_event("init") if cmds.about(batch=True): log.warning("Running batch mode ...") return # Keep reference to the main Window, once a main window exists. lib.get_main_window() def _on_scene_new(*args): emit_event("new") def _after_scene_save(*arg): emit_event("after.save") def _on_scene_save(*args): emit_event("save") def _on_scene_open(*args): emit_event("open") def _before_close_maya(*args): emit_event("before.close") def _before_scene_open(*args): emit_event("before.file.open") def _before_scene_save(return_code, client_data): # Default to allowing the action. Registered # callbacks can optionally set this to False # in order to block the operation. OpenMaya.MScriptUtil.setBool(return_code, True) emit_event( "before.save", {"return_code": return_code} ) def _remove_workfile_lock(): """Remove workfile lock on current file""" if not handle_workfile_locks(): return filepath = current_file() log.info("Removing lock on current file {}...".format(filepath)) if filepath: remove_workfile_lock(filepath) def handle_workfile_locks(): if lib.IS_HEADLESS: return False project_name = get_current_project_name() return is_workfile_lock_enabled(MayaHost.name, project_name) def uninstall(): pyblish.api.deregister_plugin_path(PUBLISH_PATH) pyblish.api.deregister_host("mayabatch") pyblish.api.deregister_host("mayapy") pyblish.api.deregister_host("maya") deregister_loader_plugin_path(LOAD_PATH) deregister_creator_plugin_path(CREATE_PATH) deregister_inventory_action_path(INVENTORY_PATH) menu.uninstall() def parse_container(container): """Return the container node's full container data. Args: container (str): A container node name. Returns: dict: The container schema data for this container node. """ data = lib.read(container) # Backwards compatibility pre-schemas for containers data["schema"] = data.get("schema", "openpype:container-1.0") # Append transient data data["objectName"] = container return data def _ls(): """Yields Avalon container node names. Used by `ls()` to retrieve the nodes and then query the full container's data. Yields: str: Avalon container node name (objectSet) """ def _maya_iterate(iterator): """Helper to iterate a maya iterator""" while not iterator.isDone(): yield iterator.thisNode() iterator.next() ids = {AVALON_CONTAINER_ID, # Backwards compatibility "pyblish.mindbender.container"} # Iterate over all 'set' nodes in the scene to detect whether # they have the avalon container ".id" attribute. fn_dep = om.MFnDependencyNode() iterator = om.MItDependencyNodes(om.MFn.kSet) for mobject in _maya_iterate(iterator): if mobject.apiTypeStr != "kSet": # Only match by exact type continue fn_dep.setObject(mobject) if not fn_dep.hasAttribute("id"): continue plug = fn_dep.findPlug("id", True) value = plug.asString() if value in ids: yield fn_dep.name() def ls(): """Yields containers from active Maya scene This is the host-equivalent of api.ls(), but instead of listing assets on disk, it lists assets already loaded in Maya; once loaded they are called 'containers' Yields: dict: container """ container_names = _ls() for container in sorted(container_names): yield parse_container(container) def containerise(name, namespace, nodes, context, loader=None, suffix="CON"): """Bundle `nodes` into an assembly and imprint it with metadata Containerisation enables a tracking of version, author and origin for loaded assets. Arguments: name (str): Name of resulting assembly namespace (str): Namespace under which to host container nodes (list): Long names of nodes to containerise context (dict): Asset information loader (str, optional): Name of loader used to produce this container. suffix (str, optional): Suffix of container, defaults to `_CON`. Returns: container (str): Name of container assembly """ container = cmds.sets(nodes, name="%s_%s_%s" % (namespace, name, suffix)) data = [ ("schema", "openpype:container-2.0"), ("id", AVALON_CONTAINER_ID), ("name", name), ("namespace", namespace), ("loader", loader), ("representation", context["representation"]["_id"]), ] for key, value in data: cmds.addAttr(container, longName=key, dataType="string") cmds.setAttr(container + "." + key, str(value), type="string") main_container = cmds.ls(AVALON_CONTAINERS, type="objectSet") if not main_container: main_container = cmds.sets(empty=True, name=AVALON_CONTAINERS) # Implement #399: Maya 2019+ hide AVALON_CONTAINERS on creation.. if cmds.attributeQuery("hiddenInOutliner", node=main_container, exists=True): cmds.setAttr(main_container + ".hiddenInOutliner", True) else: main_container = main_container[0] cmds.sets(container, addElement=main_container) # Implement #399: Maya 2019+ hide containers in outliner if cmds.attributeQuery("hiddenInOutliner", node=container, exists=True): cmds.setAttr(container + ".hiddenInOutliner", True) return container def on_init(): log.info("Running callback on init..") def safe_deferred(fn): """Execute deferred the function in a try-except""" def _fn(): """safely call in deferred callback""" try: fn() except Exception as exc: print(exc) try: utils.executeDeferred(_fn) except Exception as exc: print(exc) # Force load Alembic so referenced alembics # work correctly on scene open cmds.loadPlugin("AbcImport", quiet=True) cmds.loadPlugin("AbcExport", quiet=True) # Force load objExport plug-in (requested by artists) cmds.loadPlugin("objExport", quiet=True) if not lib.IS_HEADLESS: launch_workfiles = os.environ.get("WORKFILES_STARTUP") if launch_workfiles: safe_deferred(host_tools.show_workfiles) from .customize import ( override_component_mask_commands, override_toolbox_ui ) safe_deferred(override_component_mask_commands) safe_deferred(override_toolbox_ui) def on_before_save(): """Run validation for scene's FPS prior to saving""" return lib.validate_fps() def on_after_save(): """Check if there is a lockfile after save""" check_lock_on_current_file() def check_lock_on_current_file(): """Check if there is a user opening the file""" if not handle_workfile_locks(): return log.info("Running callback on checking the lock file...") # add the lock file when opening the file filepath = current_file() # Skip if current file is 'untitled' if not filepath: return if is_workfile_locked(filepath): # add lockfile dialog workfile_dialog = WorkfileLockDialog(filepath) if not workfile_dialog.exec_(): cmds.file(new=True) return create_workfile_lock(filepath) def on_before_close(): """Delete the lock file after user quitting the Maya Scene""" log.info("Closing Maya...") # delete the lock file filepath = current_file() if handle_workfile_locks(): remove_workfile_lock(filepath) def before_file_open(): """check lock file when the file changed""" # delete the lock file _remove_workfile_lock() def on_save(): """Automatically add IDs to new nodes Any transform of a mesh, without an existing ID, is given one automatically on file save. """ log.info("Running callback on save..") # remove lockfile if users jumps over from one scene to another _remove_workfile_lock() # Generate ids of the current context on nodes in the scene nodes = lib.get_id_required_nodes(referenced_nodes=False) for node, new_id in lib.generate_ids(nodes): lib.set_id(node, new_id, overwrite=False) def on_open(): """On scene open let's assume the containers have changed.""" from openpype.widgets import popup # Validate FPS after update_task_from_path to # ensure it is using correct FPS for the asset lib.validate_fps() lib.fix_incompatible_containers() if any_outdated_containers(): log.warning("Scene has outdated content.") # Find maya main window parent = lib.get_main_window() if parent is None: log.info("Skipping outdated content pop-up " "because Maya window can't be found.") else: # Show outdated pop-up def _on_show_inventory(): host_tools.show_scene_inventory(parent=parent) dialog = popup.Popup(parent=parent) dialog.setWindowTitle("Maya scene has outdated content") dialog.setMessage("There are outdated containers in " "your Maya scene.") dialog.on_clicked.connect(_on_show_inventory) dialog.show() # create lock file for the maya scene check_lock_on_current_file() def on_new(): """Set project resolution and fps when create a new file""" log.info("Running callback on new..") with lib.suspended_refresh(): lib.set_context_settings() _remove_workfile_lock() def on_task_changed(): """Wrapped function of app initialize and maya's on task changed""" # Run menu.update_menu_task_label() workdir = legacy_io.Session["AVALON_WORKDIR"] if os.path.exists(workdir): log.info("Updating Maya workspace for task change to %s", workdir) _set_project() # Set Maya fileDialog's start-dir to /scenes frule_scene = cmds.workspace(fileRuleEntry="scene") cmds.optionVar(stringValue=("browserLocationmayaBinaryscene", workdir + "/" + frule_scene)) else: log.warning(( "Can't set project for new context because path does not exist: {}" ).format(workdir)) with lib.suspended_refresh(): lib.set_context_settings() lib.update_content_on_context_change() def before_workfile_open(): if handle_workfile_locks(): _remove_workfile_lock() def before_workfile_save(event): project_name = get_current_project_name() if handle_workfile_locks(): _remove_workfile_lock() workdir_path = event["workdir_path"] if workdir_path: create_workspace_mel(workdir_path, project_name) def workfile_save_before_xgen(event): """Manage Xgen external files when switching context. Xgen has various external files that needs to be unique and relative to the workfile, so we need to copy and potentially overwrite these files when switching context. Args: event (Event) - openpype/lib/events.py """ if not cmds.pluginInfo("xgenToolkit", query=True, loaded=True): return import xgenm current_work_dir = legacy_io.Session["AVALON_WORKDIR"].replace("\\", "/") expected_work_dir = event.data["workdir_path"].replace("\\", "/") if current_work_dir == expected_work_dir: return palettes = cmds.ls(type="xgmPalette", long=True) if not palettes: return transfers = [] overwrites = [] attribute_changes = {} attrs = ["xgFileName", "xgBaseFile"] for palette in palettes: sanitized_palette = palette.replace("|", "") project_path = xgenm.getAttr("xgProjectPath", sanitized_palette) _, maya_extension = os.path.splitext(event.data["filename"]) for attr in attrs: node_attr = "{}.{}".format(palette, attr) attr_value = cmds.getAttr(node_attr) if not attr_value: continue source = os.path.join(project_path, attr_value) attr_value = event.data["filename"].replace( maya_extension, "__{}{}".format( sanitized_palette.replace(":", "__"), os.path.splitext(attr_value)[1] ) ) target = os.path.join(expected_work_dir, attr_value) transfers.append((source, target)) attribute_changes[node_attr] = attr_value relative_path = xgenm.getAttr( "xgDataPath", sanitized_palette ).split(os.pathsep)[0] absolute_path = relative_path.replace("${PROJECT}", project_path) for root, _, files in os.walk(absolute_path): for f in files: source = os.path.join(root, f).replace("\\", "/") target = source.replace(project_path, expected_work_dir + "/") transfers.append((source, target)) if os.path.exists(target): overwrites.append(target) # Ask user about overwriting files. if overwrites: log.warning( "WARNING! Potential loss of data.\n\n" "Found duplicate Xgen files in new context.\n{}".format( "\n".join(overwrites) ) ) return for source, destination in transfers: if not os.path.exists(os.path.dirname(destination)): os.makedirs(os.path.dirname(destination)) shutil.copy(source, destination) for attribute, value in attribute_changes.items(): cmds.setAttr(attribute, value, type="string") def after_workfile_save(event): workfile_name = event["filename"] if ( handle_workfile_locks() and workfile_name and not is_workfile_locked(workfile_name) ): create_workfile_lock(workfile_name) class MayaDirmap(HostDirmap): def on_enable_dirmap(self): cmds.dirmap(en=True) def dirmap_routine(self, source_path, destination_path): cmds.dirmap(m=(source_path, destination_path)) cmds.dirmap(m=(destination_path, source_path)) ================================================ FILE: openpype/hosts/maya/api/plugin.py ================================================ import json import os from abc import ABCMeta import qargparse import six from maya import cmds from maya.app.renderSetup.model import renderSetup from openpype import AYON_SERVER_ENABLED from openpype.lib import BoolDef, Logger from openpype.settings import get_project_settings from openpype.pipeline import ( AVALON_CONTAINER_ID, Anatomy, CreatedInstance, Creator as NewCreator, AutoCreator, HiddenCreator, CreatorError, LegacyCreator, LoaderPlugin, get_representation_path, ) from openpype.pipeline.load import LoadError from openpype.client import get_asset_by_name from openpype.pipeline.create import get_subset_name from . import lib from .lib import imprint, read from .pipeline import containerise log = Logger.get_logger() def _get_attr(node, attr, default=None): """Helper to get attribute which allows attribute to not exist.""" if not cmds.attributeQuery(attr, node=node, exists=True): return default return cmds.getAttr("{}.{}".format(node, attr)) # Backwards compatibility: these functions has been moved to lib. def get_reference_node(*args, **kwargs): """Get the reference node from the container members Deprecated: This function was moved and will be removed in 3.16.x. """ msg = "Function 'get_reference_node' has been moved." log.warning(msg) cmds.warning(msg) return lib.get_reference_node(*args, **kwargs) def get_reference_node_parents(*args, **kwargs): """ Deprecated: This function was moved and will be removed in 3.16.x. """ msg = "Function 'get_reference_node_parents' has been moved." log.warning(msg) cmds.warning(msg) return lib.get_reference_node_parents(*args, **kwargs) class Creator(LegacyCreator): defaults = ['Main'] def process(self): nodes = list() with lib.undo_chunk(): if (self.options or {}).get("useSelection"): nodes = cmds.ls(selection=True) instance = cmds.sets(nodes, name=self.name) lib.imprint(instance, self.data) return instance @six.add_metaclass(ABCMeta) class MayaCreatorBase(object): @staticmethod def cache_subsets(shared_data): """Cache instances for Creators to shared data. Create `maya_cached_subsets` key when needed in shared data and fill it with all collected instances from the scene under its respective creator identifiers. If legacy instances are detected in the scene, create `maya_cached_legacy_subsets` there and fill it with all legacy subsets under family as a key. Args: Dict[str, Any]: Shared data. Return: Dict[str, Any]: Shared data dictionary. """ if shared_data.get("maya_cached_subsets") is None: cache = dict() cache_legacy = dict() for node in cmds.ls(type="objectSet"): if _get_attr(node, attr="id") != "pyblish.avalon.instance": continue creator_id = _get_attr(node, attr="creator_identifier") if creator_id is not None: # creator instance cache.setdefault(creator_id, []).append(node) else: # legacy instance family = _get_attr(node, attr="family") if family is None: # must be a broken instance continue cache_legacy.setdefault(family, []).append(node) shared_data["maya_cached_subsets"] = cache shared_data["maya_cached_legacy_subsets"] = cache_legacy return shared_data def get_publish_families(self): """Return families for the instances of this creator. Allow a Creator to define multiple families so that a creator can e.g. specify `usd` and `usdMaya` and another USD creator can also specify `usd` but apply different extractors like `usdMultiverse`. There is no need to override this method if you only have the primary family defined by the `family` property as that will always be set. Returns: list: families for instances of this creator """ return [] def imprint_instance_node(self, node, data): # We never store the instance_node as value on the node since # it's the node name itself data.pop("instance_node", None) data.pop("instance_id", None) # Don't store `families` since it's up to the creator itself # to define the initial publish families - not a stored attribute of # `families` data.pop("families", None) # We store creator attributes at the root level and assume they # will not clash in names with `subset`, `task`, etc. and other # default names. This is just so these attributes in many cases # are still editable in the maya UI by artists. # note: pop to move to end of dict to sort attributes last on the node creator_attributes = data.pop("creator_attributes", {}) # We only flatten value types which `imprint` function supports json_creator_attributes = {} for key, value in dict(creator_attributes).items(): if isinstance(value, (list, tuple, dict)): creator_attributes.pop(key) json_creator_attributes[key] = value # Flatten remaining creator attributes to the node itself data.update(creator_attributes) # We know the "publish_attributes" will be complex data of # settings per plugins, we'll store this as a flattened json structure # pop to move to end of dict to sort attributes last on the node data["publish_attributes"] = json.dumps( data.pop("publish_attributes", {}) ) # Persist the non-flattened creator attributes (special value types, # like multiselection EnumDef) data["creator_attributes"] = json.dumps(json_creator_attributes) # Since we flattened the data structure for creator attributes we want # to correctly detect which flattened attributes should end back in the # creator attributes when reading the data from the node, so we store # the relevant keys as a string data["__creator_attributes_keys"] = ",".join(creator_attributes.keys()) # Kill any existing attributes just so we can imprint cleanly again for attr in data.keys(): if cmds.attributeQuery(attr, node=node, exists=True): cmds.deleteAttr("{}.{}".format(node, attr)) return imprint(node, data) def read_instance_node(self, node): node_data = read(node) # Never care about a cbId attribute on the object set # being read as 'data' node_data.pop("cbId", None) # Make sure we convert any creator attributes from the json string creator_attributes = node_data.get("creator_attributes") if creator_attributes: node_data["creator_attributes"] = json.loads(creator_attributes) else: node_data["creator_attributes"] = {} # Move the relevant attributes into "creator_attributes" that # we flattened originally creator_attribute_keys = node_data.pop("__creator_attributes_keys", "").split(",") for key in creator_attribute_keys: if key in node_data: node_data["creator_attributes"][key] = node_data.pop(key) # Make sure we convert any publish attributes from the json string publish_attributes = node_data.get("publish_attributes") if publish_attributes: node_data["publish_attributes"] = json.loads(publish_attributes) # Explicitly re-parse the node name node_data["instance_node"] = node node_data["instance_id"] = node # If the creator plug-in specifies families = self.get_publish_families() if families: node_data["families"] = families return node_data def _default_collect_instances(self): self.cache_subsets(self.collection_shared_data) cached_subsets = self.collection_shared_data["maya_cached_subsets"] for node in cached_subsets.get(self.identifier, []): node_data = self.read_instance_node(node) created_instance = CreatedInstance.from_existing(node_data, self) self._add_instance_to_context(created_instance) def _default_update_instances(self, update_list): for created_inst, _changes in update_list: data = created_inst.data_to_store() node = data.get("instance_node") self.imprint_instance_node(node, data) def _default_remove_instances(self, instances): """Remove specified instance from the scene. This is only removing `id` parameter so instance is no longer instance, because it might contain valuable data for artist. """ for instance in instances: node = instance.data.get("instance_node") if node: cmds.delete(node) self._remove_instance_from_context(instance) @six.add_metaclass(ABCMeta) class MayaCreator(NewCreator, MayaCreatorBase): settings_category = "maya" def create(self, subset_name, instance_data, pre_create_data): members = list() if pre_create_data.get("use_selection"): members = cmds.ls(selection=True) # Allow a Creator to define multiple families publish_families = self.get_publish_families() if publish_families: families = instance_data.setdefault("families", []) for family in self.get_publish_families(): if family not in families: families.append(family) with lib.undo_chunk(): instance_node = cmds.sets(members, name=subset_name) instance_data["instance_node"] = instance_node instance = CreatedInstance( self.family, subset_name, instance_data, self) self._add_instance_to_context(instance) self.imprint_instance_node(instance_node, data=instance.data_to_store()) return instance def collect_instances(self): return self._default_collect_instances() def update_instances(self, update_list): return self._default_update_instances(update_list) def remove_instances(self, instances): return self._default_remove_instances(instances) def get_pre_create_attr_defs(self): return [ BoolDef("use_selection", label="Use selection", default=True) ] class MayaAutoCreator(AutoCreator, MayaCreatorBase): """Automatically triggered creator for Maya. The plugin is not visible in UI, and 'create' method does not expect any arguments. """ settings_category = "maya" def collect_instances(self): return self._default_collect_instances() def update_instances(self, update_list): return self._default_update_instances(update_list) def remove_instances(self, instances): return self._default_remove_instances(instances) class MayaHiddenCreator(HiddenCreator, MayaCreatorBase): """Hidden creator for Maya. The plugin is not visible in UI, and it does not have strictly defined arguments for 'create' method. """ settings_category = "maya" def create(self, *args, **kwargs): return MayaCreator.create(self, *args, **kwargs) def collect_instances(self): return self._default_collect_instances() def update_instances(self, update_list): return self._default_update_instances(update_list) def remove_instances(self, instances): return self._default_remove_instances(instances) def ensure_namespace(namespace): """Make sure the namespace exists. Args: namespace (str): The preferred namespace name. Returns: str: The generated or existing namespace """ exists = cmds.namespace(exists=namespace) if exists: return namespace else: return cmds.namespace(add=namespace) class RenderlayerCreator(NewCreator, MayaCreatorBase): """Creator which creates an instance per renderlayer in the workfile. Create and manages renderlayer subset per renderLayer in workfile. This generates a singleton node in the scene which, if it exists, tells the Creator to collect Maya rendersetup renderlayers as individual instances. As such, triggering create doesn't actually create the instance node per layer but only the node which tells the Creator it may now collect an instance per renderlayer. """ # These are required to be overridden in subclass singleton_node_name = "" # These are optional to be overridden in subclass layer_instance_prefix = None def _get_singleton_node(self, return_all=False): nodes = lib.lsattr("pre_creator_identifier", self.identifier) if nodes: return nodes if return_all else nodes[0] def create(self, subset_name, instance_data, pre_create_data): # A Renderlayer is never explicitly created using the create method. # Instead, renderlayers from the scene are collected. Thus "create" # would only ever be called to say, 'hey, please refresh collect' self.create_singleton_node() # if no render layers are present, create default one with # asterisk selector rs = renderSetup.instance() if not rs.getRenderLayers(): render_layer = rs.createRenderLayer("Main") collection = render_layer.createCollection("defaultCollection") collection.getSelector().setPattern('*') # By RenderLayerCreator.create we make it so that the renderlayer # instances directly appear even though it just collects scene # renderlayers. This doesn't actually 'create' any scene contents. self.collect_instances() def create_singleton_node(self): if self._get_singleton_node(): raise CreatorError("A Render instance already exists - only " "one can be configured.") with lib.undo_chunk(): node = cmds.sets(empty=True, name=self.singleton_node_name) lib.imprint(node, data={ "pre_creator_identifier": self.identifier }) return node def collect_instances(self): # We only collect if the global render instance exists if not self._get_singleton_node(): return rs = renderSetup.instance() layers = rs.getRenderLayers() for layer in layers: layer_instance_node = self.find_layer_instance_node(layer) if layer_instance_node: data = self.read_instance_node(layer_instance_node) instance = CreatedInstance.from_existing(data, creator=self) else: # No existing scene instance node for this layer. Note that # this instance will not have the `instance_node` data yet # until it's been saved/persisted at least once. project_name = self.create_context.get_current_project_name() asset_name = self.create_context.get_current_asset_name() instance_data = { "task": self.create_context.get_current_task_name(), "variant": layer.name(), } if AYON_SERVER_ENABLED: instance_data["folderPath"] = asset_name else: instance_data["asset"] = asset_name asset_doc = get_asset_by_name(project_name, asset_name) subset_name = self.get_subset_name( layer.name(), instance_data["task"], asset_doc, project_name) instance = CreatedInstance( family=self.family, subset_name=subset_name, data=instance_data, creator=self ) instance.transient_data["layer"] = layer self._add_instance_to_context(instance) def find_layer_instance_node(self, layer): connected_sets = cmds.listConnections( "{}.message".format(layer.name()), source=False, destination=True, type="objectSet" ) or [] for node in connected_sets: if not cmds.attributeQuery("creator_identifier", node=node, exists=True): continue creator_identifier = cmds.getAttr(node + ".creator_identifier") if creator_identifier == self.identifier: self.log.info("Found node: {}".format(node)) return node def _create_layer_instance_node(self, layer): # We only collect if a CreateRender instance exists create_render_set = self._get_singleton_node() if not create_render_set: raise CreatorError("Creating a renderlayer instance node is not " "allowed if no 'CreateRender' instance exists") namespace = "_{}".format(self.singleton_node_name) namespace = ensure_namespace(namespace) name = "{}:{}".format(namespace, layer.name()) render_set = cmds.sets(name=name, empty=True) # Keep an active link with the renderlayer so we can retrieve it # later by a physical maya connection instead of relying on the layer # name cmds.addAttr(render_set, longName="renderlayer", at="message") cmds.connectAttr("{}.message".format(layer.name()), "{}.renderlayer".format(render_set), force=True) # Add the set to the 'CreateRender' set. cmds.sets(render_set, forceElement=create_render_set) return render_set def update_instances(self, update_list): # We only generate the persisting layer data into the scene once # we save with the UI on e.g. validate or publish for instance, _changes in update_list: instance_node = instance.data.get("instance_node") # Ensure a node exists to persist the data to if not instance_node: layer = instance.transient_data["layer"] instance_node = self._create_layer_instance_node(layer) instance.data["instance_node"] = instance_node self.imprint_instance_node(instance_node, data=instance.data_to_store()) def imprint_instance_node(self, node, data): # Do not ever try to update the `renderlayer` since it'll try # to remove the attribute and recreate it but fail to keep it a # message attribute link. We only ever imprint that on the initial # node creation. # TODO: Improve how this is handled data.pop("renderlayer", None) data.get("creator_attributes", {}).pop("renderlayer", None) return super(RenderlayerCreator, self).imprint_instance_node(node, data=data) def remove_instances(self, instances): """Remove specified instances from the scene. This is only removing `id` parameter so instance is no longer instance, because it might contain valuable data for artist. """ # Instead of removing the single instance or renderlayers we instead # remove the CreateRender node this creator relies on to decide whether # it should collect anything at all. nodes = self._get_singleton_node(return_all=True) if nodes: cmds.delete(nodes) # Remove ALL the instances even if only one gets deleted for instance in list(self.create_context.instances): if instance.get("creator_identifier") == self.identifier: self._remove_instance_from_context(instance) # Remove the stored settings per renderlayer too node = instance.data.get("instance_node") if node and cmds.objExists(node): cmds.delete(node) def get_subset_name( self, variant, task_name, asset_doc, project_name, host_name=None, instance=None ): # creator.family != 'render' as expected return get_subset_name(self.layer_instance_prefix, variant, task_name, asset_doc, project_name) class Loader(LoaderPlugin): hosts = ["maya"] load_settings = {} # defined in settings @classmethod def apply_settings(cls, project_settings, system_settings): super(Loader, cls).apply_settings(project_settings, system_settings) cls.load_settings = project_settings['maya']['load'] def get_custom_namespace_and_group(self, context, options, loader_key): """Queries Settings to get custom template for namespace and group. Group template might be empty >> this forces to not wrap imported items into separate group. Args: context (dict) options (dict): artist modifiable options from dialog loader_key (str): key to get separate configuration from Settings ('reference_loader'|'import_loader') """ options["attach_to_root"] = True try: custom_naming = self.load_settings[loader_key] except KeyError: self.log.warning( "No settings found for {} in settings, falling back to " "ReferenceLoader defaults.".format(loader_key)) custom_naming = self.load_settings["reference_loader"] if not custom_naming['namespace']: raise LoadError("No namespace specified in " "Maya ReferenceLoader settings") elif not custom_naming['group_name']: self.log.debug("No custom group_name, no group will be created.") options["attach_to_root"] = False asset = context['asset'] subset = context['subset'] formatting_data = { "asset_name": asset['name'], "asset_type": asset['type'], "folder": { "name": asset["name"], }, "subset": subset['name'], "family": ( subset['data'].get('family') or subset['data']['families'][0] ) } custom_namespace = custom_naming['namespace'].format( **formatting_data ) custom_group_name = custom_naming['group_name'].format( **formatting_data ) return custom_group_name, custom_namespace, options class ReferenceLoader(Loader): """A basic ReferenceLoader for Maya This will implement the basic behavior for a loader to inherit from that will containerize the reference and will implement the `remove` and `update` logic. """ options = [ qargparse.Integer( "count", label="Count", default=1, min=1, help="How many times to load?" ), qargparse.Double3( "offset", label="Position Offset", help="Offset loaded models for easier selection." ), qargparse.Boolean( "attach_to_root", label="Group imported asset", default=True, help="Should a group be created to encapsulate" " imported representation ?" ) ] def load( self, context, name=None, namespace=None, options=None ): path = self.filepath_from_context(context) assert os.path.exists(path), "%s does not exist." % path custom_group_name, custom_namespace, options = \ self.get_custom_namespace_and_group(context, options, "reference_loader") count = options.get("count") or 1 loaded_containers = [] for c in range(0, count): namespace = lib.get_custom_namespace(custom_namespace) group_name = "{}:{}".format( namespace, custom_group_name ) options['group_name'] = group_name # Offset loaded subset if "offset" in options: offset = [i * c for i in options["offset"]] options["translate"] = offset self.log.info(options) self.process_reference( context=context, name=name, namespace=namespace, options=options ) # Only containerize if any nodes were loaded by the Loader nodes = self[:] if not nodes: return ref_node = lib.get_reference_node(nodes, self.log) container = containerise( name=name, namespace=namespace, nodes=[ref_node], context=context, loader=self.__class__.__name__ ) loaded_containers.append(container) self._organize_containers(nodes, container) c += 1 return loaded_containers def process_reference(self, context, name, namespace, options): """To be implemented by subclass""" raise NotImplementedError("Must be implemented by subclass") def update(self, container, representation): from maya import cmds from openpype.hosts.maya.api.lib import get_container_members node = container["objectName"] path = get_representation_path(representation) # Get reference node from container members members = get_container_members(node) reference_node = lib.get_reference_node(members, self.log) namespace = cmds.referenceQuery(reference_node, namespace=True) file_type = { "ma": "mayaAscii", "mb": "mayaBinary", "abc": "Alembic", "fbx": "FBX", "usd": "USD Import" }.get(representation["name"]) assert file_type, "Unsupported representation: %s" % representation assert os.path.exists(path), "%s does not exist." % path # Need to save alembic settings and reapply, cause referencing resets # them to incoming data. alembic_attrs = ["speed", "offset", "cycleType", "time"] alembic_data = {} if representation["name"] == "abc": alembic_nodes = cmds.ls( "{}:*".format(namespace), type="AlembicNode" ) if alembic_nodes: for attr in alembic_attrs: node_attr = "{}.{}".format(alembic_nodes[0], attr) data = { "input": lib.get_attribute_input(node_attr), "value": cmds.getAttr(node_attr) } alembic_data[attr] = data else: self.log.debug("No alembic nodes found in {}".format(members)) try: path = self.prepare_root_value(path, representation["context"] ["project"] ["name"]) content = cmds.file(path, loadReference=reference_node, type=file_type, returnNewNodes=True) except RuntimeError as exc: # When changing a reference to a file that has load errors the # command will raise an error even if the file is still loaded # correctly (e.g. when raising errors on Arnold attributes) # When the file is loaded and has content, we consider it's fine. if not cmds.referenceQuery(reference_node, isLoaded=True): raise content = cmds.referenceQuery(reference_node, nodes=True, dagPath=True) if not content: raise self.log.warning("Ignoring file read error:\n%s", exc) self._organize_containers(content, container["objectName"]) # Reapply alembic settings. if representation["name"] == "abc" and alembic_data: alembic_nodes = cmds.ls( "{}:*".format(namespace), type="AlembicNode" ) if alembic_nodes: alembic_node = alembic_nodes[0] # assume single AlembicNode for attr, data in alembic_data.items(): node_attr = "{}.{}".format(alembic_node, attr) input = lib.get_attribute_input(node_attr) if data["input"]: if data["input"] != input: cmds.connectAttr( data["input"], node_attr, force=True ) else: if input: cmds.disconnectAttr(input, node_attr) cmds.setAttr(node_attr, data["value"]) # Fix PLN-40 for older containers created with Avalon that had the # `.verticesOnlySet` set to True. if cmds.getAttr("{}.verticesOnlySet".format(node)): self.log.info("Setting %s.verticesOnlySet to False", node) cmds.setAttr("{}.verticesOnlySet".format(node), False) # Remove any placeHolderList attribute entries from the set that # are remaining from nodes being removed from the referenced file. members = cmds.sets(node, query=True) invalid = [x for x in members if ".placeHolderList" in x] if invalid: cmds.sets(invalid, remove=node) # Update metadata cmds.setAttr("{}.representation".format(node), str(representation["_id"]), type="string") # When an animation or pointcache gets connected to an Xgen container, # the compound attribute "xgenContainers" gets created. When animation # containers gets updated we also need to update the cacheFileName on # the Xgen collection. compound_name = "xgenContainers" if cmds.objExists("{}.{}".format(node, compound_name)): import xgenm container_amount = cmds.getAttr( "{}.{}".format(node, compound_name), size=True ) # loop through all compound children for i in range(container_amount): attr = "{}.{}[{}].container".format(node, compound_name, i) objectset = cmds.listConnections(attr)[0] reference_node = cmds.sets(objectset, query=True)[0] palettes = cmds.ls( cmds.referenceQuery(reference_node, nodes=True), type="xgmPalette" ) for palette in palettes: for description in xgenm.descriptions(palette): xgenm.setAttr( "cacheFileName", path.replace("\\", "/"), palette, description, "SplinePrimitive" ) # Refresh UI and viewport. de = xgenm.xgGlobal.DescriptionEditor de.refresh("Full") def remove(self, container): """Remove an existing `container` from Maya scene Deprecated; this functionality is replaced by `api.remove()` Arguments: container (openpype:container-1.0): Which container to remove from scene. """ from maya import cmds node = container["objectName"] # Assume asset has been referenced members = cmds.sets(node, query=True) reference_node = lib.get_reference_node(members, self.log) assert reference_node, ("Imported container not supported; " "container must be referenced.") self.log.info("Removing '%s' from Maya.." % container["name"]) namespace = cmds.referenceQuery(reference_node, namespace=True) fname = cmds.referenceQuery(reference_node, filename=True) cmds.file(fname, removeReference=True) try: cmds.delete(node) except ValueError: # Already implicitly deleted by Maya upon removing reference pass try: # If container is not automatically cleaned up by May (issue #118) cmds.namespace(removeNamespace=namespace, deleteNamespaceContent=True) except RuntimeError: pass def prepare_root_value(self, file_url, project_name): """Replace root value with env var placeholder. Use ${OPENPYPE_ROOT_WORK} (or any other root) instead of proper root value when storing referenced url into a workfile. Useful for remote workflows with SiteSync. Args: file_url (str) project_name (dict) Returns: (str) """ settings = get_project_settings(project_name) use_env_var_as_root = (settings["maya"] ["maya-dirmap"] ["use_env_var_as_root"]) if use_env_var_as_root: anatomy = Anatomy(project_name) file_url = anatomy.replace_root_with_env_key(file_url, '${{{}}}') return file_url @staticmethod def _organize_containers(nodes, container): # type: (list, str) -> None """Put containers in loaded data to correct hierarchy.""" for node in nodes: id_attr = "{}.id".format(node) if not cmds.attributeQuery("id", node=node, exists=True): continue if cmds.getAttr(id_attr) == AVALON_CONTAINER_ID: cmds.sets(node, forceElement=container) ================================================ FILE: openpype/hosts/maya/api/render_setup_tools.py ================================================ # -*- coding: utf-8 -*- """Export stuff in render setup layer context. Export Maya nodes from Render Setup layer as if flattened in that layer instead of exporting the defaultRenderLayer as Maya forces by default Credits: Roy Nieterau (BigRoy) / Colorbleed Modified for use in OpenPype """ import os import contextlib from maya import cmds from maya.app.renderSetup.model import renderSetup from .lib import pairwise @contextlib.contextmanager def _allow_export_from_render_setup_layer(): """Context manager to override Maya settings to allow RS layer export""" try: rs = renderSetup.instance() # Exclude Render Setup nodes from the export rs._setAllRSNodesDoNotWrite(True) # Disable Render Setup forcing the switch to master layer os.environ["MAYA_BATCH_RENDER_EXPORT"] = "1" yield finally: # Reset original state rs._setAllRSNodesDoNotWrite(False) os.environ.pop("MAYA_BATCH_RENDER_EXPORT", None) def export_in_rs_layer(path, nodes, export=None): """Export nodes from Render Setup layer. When exporting from Render Setup layer Maya by default forces a switch to the defaultRenderLayer as such making it impossible to export the contents of a Render Setup layer. Maya presents this warning message: # Warning: Exporting Render Setup master layer content # This function however avoids the renderlayer switch and exports from the Render Setup layer as if the edits were 'flattened' in the master layer. It does so by: - Allowing export from Render Setup Layer - Enforce Render Setup nodes to NOT be written on export - Disconnect connections from any `applyOverride` nodes to flatten the values (so they are written correctly)* *Connection overrides like Shader Override and Material Overrides export correctly out of the box since they don't create an intermediate connection to an 'applyOverride' node. However, any scalar override (absolute or relative override) will get input connections in the layer so we'll break those to 'store' the values on the attribute itself and write value out instead. Args: path (str): File path to export to. nodes (list): Maya nodes to export. export (callable, optional): Callback to be used for exporting. If not specified, default export to `.ma` will be called. Returns: None Raises: AssertionError: When not in a Render Setup layer an AssertionError is raised. This command assumes you are currently in a Render Setup layer. """ rs = renderSetup.instance() assert rs.getVisibleRenderLayer().name() != "defaultRenderLayer", \ ("Export in Render Setup layer is only supported when in " "Render Setup layer") # Break connection to any value overrides history = cmds.listHistory(nodes) or [] nodes_all = list( set(cmds.ls(nodes + history, long=True, objectsOnly=True))) overrides = cmds.listConnections(nodes_all, source=True, destination=False, type="applyOverride", plugs=True, connections=True) or [] for dest, src in pairwise(overrides): # Even after disconnecting the values # should be preserved as they were # Note: animated overrides would be lost for export cmds.disconnectAttr(src, dest) # Export Selected with _allow_export_from_render_setup_layer(): cmds.select(nodes, noExpand=True) if export: export() else: cmds.file(path, force=True, typ="mayaAscii", exportSelected=True, preserveReferences=False, channels=True, constraints=True, expressions=True, constructionHistory=True) if overrides: # If we have broken override connections then Maya # is unaware that the Render Setup layer is in an # invalid state. So let's 'hard reset' the state # by going to default render layer and switching back layer = rs.getVisibleRenderLayer() rs.switchToLayer(None) rs.switchToLayer(layer) ================================================ FILE: openpype/hosts/maya/api/setdress.py ================================================ import logging import json import os import contextlib import copy import six from maya import cmds from openpype.client import ( get_version_by_name, get_last_version_by_subset_id, get_representation_by_id, get_representation_by_name, get_representation_parents, ) from openpype.pipeline import ( schema, discover_loader_plugins, loaders_from_representation, load_container, update_container, remove_container, get_representation_path, get_current_project_name, ) from openpype.hosts.maya.api.lib import ( matrix_equals, unique_namespace, get_container_transforms, DEFAULT_MATRIX ) log = logging.getLogger("PackageLoader") def to_namespace(node, namespace): """Return node name as if it's inside the namespace. Args: node (str): Node name namespace (str): Namespace Returns: str: The node in the namespace. """ namespace_prefix = "|{}:".format(namespace) node = namespace_prefix.join(node.split("|")) return node @contextlib.contextmanager def namespaced(namespace, new=True): """Work inside namespace during context Args: new (bool): When enabled this will rename the namespace to a unique namespace if the input namespace already exists. Yields: str: The namespace that is used during the context """ original = cmds.namespaceInfo(cur=True) if new: namespace = unique_namespace(namespace) cmds.namespace(add=namespace) try: cmds.namespace(set=namespace) yield namespace finally: cmds.namespace(set=original) @contextlib.contextmanager def unlocked(nodes): # Get node state by Maya's uuid nodes = cmds.ls(nodes, long=True) uuids = cmds.ls(nodes, uuid=True) states = cmds.lockNode(nodes, query=True, lock=True) states = {uuid: state for uuid, state in zip(uuids, states)} originals = {uuid: node for uuid, node in zip(uuids, nodes)} try: cmds.lockNode(nodes, lock=False) yield finally: # Reapply original states _iteritems = getattr(states, "iteritems", states.items) for uuid, state in _iteritems(): nodes_from_id = cmds.ls(uuid, long=True) if nodes_from_id: node = nodes_from_id[0] else: log.debug("Falling back to node name: %s", node) node = originals[uuid] if not cmds.objExists(node): log.warning("Unable to find: %s", node) continue cmds.lockNode(node, lock=state) def load_package(filepath, name, namespace=None): """Load a package that was gathered elsewhere. A package is a group of published instances, possibly with additional data in a hierarchy. """ if namespace is None: # Define a unique namespace for the package namespace = os.path.basename(filepath).split(".")[0] unique_namespace(namespace) assert isinstance(namespace, six.string_types) # Load the setdress package data with open(filepath, "r") as fp: data = json.load(fp) # Load the setdress alembic hierarchy # We import this into the namespace in which we'll load the package's # instances into afterwards. alembic = filepath.replace(".json", ".abc") hierarchy = cmds.file(alembic, reference=True, namespace=namespace, returnNewNodes=True, groupReference=True, groupName="{}:{}".format(namespace, name), typ="Alembic") # Get the top root node (the reference group) root = "{}:{}".format(namespace, name) containers = [] all_loaders = discover_loader_plugins() for representation_id, instances in data.items(): # Find the compatible loaders loaders = loaders_from_representation( all_loaders, representation_id ) for instance in instances: container = _add(instance=instance, representation_id=representation_id, loaders=loaders, namespace=namespace, root=root) containers.append(container) # TODO: Do we want to cripple? Or do we want to add a 'parent' parameter? # Cripple the original avalon containers so they don't show up in the # manager # for container in containers: # cmds.setAttr("%s.id" % container, # "setdress.container", # type="string") # TODO: Lock all loaded nodes # This is to ensure the hierarchy remains unaltered by the artists # for node in nodes: # cmds.lockNode(node, lock=True) return containers + hierarchy def _add(instance, representation_id, loaders, namespace, root="|"): """Add an item from the package Args: instance (dict): representation_id (str): loaders (list): namespace (str): Returns: str: The created Avalon container. """ # Process within the namespace with namespaced(namespace, new=False) as namespace: # Get the used loader Loader = next((x for x in loaders if x.__name__ == instance['loader']), None) if Loader is None: log.warning("Loader is missing: %s. Skipping %s", instance['loader'], instance) raise RuntimeError("Loader is missing.") container = load_container( Loader, representation_id, namespace=instance['namespace'] ) # Get the root from the loaded container loaded_root = get_container_transforms({"objectName": container}, root=True) # Apply matrix to root node (if any matrix edits) matrix = instance.get("matrix", None) if matrix: cmds.xform(loaded_root, objectSpace=True, matrix=matrix) # Parent into the setdress hierarchy # Namespace is missing from parent node(s), add namespace # manually parent = root + to_namespace(instance["parent"], namespace) cmds.parent(loaded_root, parent, relative=True) return container # Store root nodes based on representation and namespace def _instances_by_namespace(data): """Rebuild instance data so we can look it up by namespace. Note that the `representation` is added into the instance's data with a `representation` key. Args: data (dict): scene build data Returns: dict """ result = {} # Add new assets for representation_id, instances in data.items(): # Ensure we leave the source data unaltered instances = copy.deepcopy(instances) for instance in instances: instance['representation'] = representation_id result[instance['namespace']] = instance return result def get_contained_containers(container): """Get the Avalon containers in this container Args: container (dict): The container dict. Returns: list: A list of member container dictionaries. """ from .pipeline import parse_container # Get avalon containers in this package setdress container containers = [] members = cmds.sets(container['objectName'], query=True) for node in cmds.ls(members, type="objectSet"): try: member_container = parse_container(node) containers.append(member_container) except schema.ValidationError: pass return containers def update_package_version(container, version): """ Update package by version number Args: container (dict): container data of the container node version (int): the new version number of the package Returns: None """ # Versioning (from `core.maya.pipeline`) project_name = get_current_project_name() current_representation = get_representation_by_id( project_name, container["representation"] ) assert current_representation is not None, "This is a bug" version_doc, subset_doc, asset_doc, project_doc = ( get_representation_parents(project_name, current_representation) ) if version == -1: new_version = get_last_version_by_subset_id( project_name, subset_doc["_id"] ) else: new_version = get_version_by_name( project_name, version, subset_doc["_id"] ) assert new_version is not None, "This is a bug" # Get the new representation (new file) new_representation = get_representation_by_name( project_name, current_representation["name"], new_version["_id"] ) update_package(container, new_representation) def update_package(set_container, representation): """Update any matrix changes in the scene based on the new data Args: set_container (dict): container data from `ls()` representation (dict): the representation document from the database Returns: None """ # Load the original package data project_name = get_current_project_name() current_representation = get_representation_by_id( project_name, set_container["representation"] ) current_file = get_representation_path(current_representation) assert current_file.endswith(".json") with open(current_file, "r") as fp: current_data = json.load(fp) # Load the new package data new_file = get_representation_path(representation) assert new_file.endswith(".json") with open(new_file, "r") as fp: new_data = json.load(fp) # Update scene content containers = get_contained_containers(set_container) update_scene(set_container, containers, current_data, new_data, new_file) # TODO: This should be handled by the pipeline itself cmds.setAttr(set_container['objectName'] + ".representation", str(representation['_id']), type="string") def update_scene(set_container, containers, current_data, new_data, new_file): """Updates the hierarchy, assets and their matrix Updates the following within the scene: * Setdress hierarchy alembic * Matrix * Parenting * Representations It removes any assets which are not present in the new build data Args: set_container (dict): the setdress container of the scene containers (list): the list of containers under the setdress container current_data (dict): the current build data of the setdress new_data (dict): the new build data of the setdres Returns: processed_containers (list): all new and updated containers """ set_namespace = set_container['namespace'] project_name = get_current_project_name() # Update the setdress hierarchy alembic set_root = get_container_transforms(set_container, root=True) set_hierarchy_root = cmds.listRelatives(set_root, fullPath=True)[0] set_hierarchy_reference = cmds.referenceQuery(set_hierarchy_root, referenceNode=True) new_alembic = new_file.replace(".json", ".abc") assert os.path.exists(new_alembic), "%s does not exist." % new_alembic with unlocked(cmds.listRelatives(set_root, ad=True, fullPath=True)): cmds.file(new_alembic, loadReference=set_hierarchy_reference, type="Alembic") identity = DEFAULT_MATRIX[:] processed_namespaces = set() processed_containers = list() new_lookup = _instances_by_namespace(new_data) old_lookup = _instances_by_namespace(current_data) for container in containers: container_ns = container['namespace'] # Consider it processed here, even it it fails we want to store that # the namespace was already available. processed_namespaces.add(container_ns) processed_containers.append(container['objectName']) if container_ns in new_lookup: root = get_container_transforms(container, root=True) if not root: log.error("Can't find root for %s", container['objectName']) continue old_instance = old_lookup.get(container_ns, {}) new_instance = new_lookup[container_ns] # Update the matrix # check matrix against old_data matrix to find local overrides current_matrix = cmds.xform(root, query=True, matrix=True, objectSpace=True) original_matrix = old_instance.get("matrix", identity) has_matrix_override = not matrix_equals(current_matrix, original_matrix) if has_matrix_override: log.warning("Matrix override preserved on %s", container_ns) else: new_matrix = new_instance.get("matrix", identity) cmds.xform(root, matrix=new_matrix, objectSpace=True) # Update the parenting if old_instance.get("parent", None) != new_instance["parent"]: parent = to_namespace(new_instance['parent'], set_namespace) if not cmds.objExists(parent): log.error("Can't find parent %s", parent) continue # Set the new parent cmds.lockNode(root, lock=False) root = cmds.parent(root, parent, relative=True) cmds.lockNode(root, lock=True) # Update the representation representation_current = container['representation'] representation_old = old_instance['representation'] representation_new = new_instance['representation'] has_representation_override = (representation_current != representation_old) if representation_new != representation_current: if has_representation_override: log.warning("Your scene had local representation " "overrides within the set. New " "representations not loaded for %s.", container_ns) continue # We check it against the current 'loader' in the scene instead # of the original data of the package that was loaded because # an Artist might have made scene local overrides if new_instance['loader'] != container['loader']: log.warning("Loader is switched - local edits will be " "lost. Removing: %s", container_ns) # Remove this from the "has been processed" list so it's # considered as new element and added afterwards. processed_containers.pop() processed_namespaces.remove(container_ns) remove_container(container) continue # Check whether the conversion can be done by the Loader. # They *must* use the same asset, subset and Loader for # `update_container` to make sense. old = get_representation_by_id( project_name, representation_current ) new = get_representation_by_id( project_name, representation_new ) is_valid = compare_representations(old=old, new=new) if not is_valid: log.error("Skipping: %s. See log for details.", container_ns) continue new_version = new["context"]["version"] update_container(container, version=new_version) else: # Remove this container because it's not in the new data log.warning("Removing content: %s", container_ns) remove_container(container) # Add new assets all_loaders = discover_loader_plugins() for representation_id, instances in new_data.items(): # Find the compatible loaders loaders = loaders_from_representation( all_loaders, representation_id ) for instance in instances: # Already processed in update functionality if instance['namespace'] in processed_namespaces: continue container = _add(instance=instance, representation_id=representation_id, loaders=loaders, namespace=set_container['namespace'], root=set_root) # Add to the setdress container cmds.sets(container, addElement=set_container['objectName']) processed_containers.append(container) return processed_containers def compare_representations(old, new): """Check if the old representation given can be updated Due to limitations of the `update_container` function we cannot allow differences in the following data: * Representation name (extension) * Asset name * Subset name (variation) If any of those data values differs, the function will raise an RuntimeError Args: old(dict): representation data from the database new(dict): representation data from the database Returns: bool: False if the representation is not invalid else True """ if new["name"] != old["name"]: log.error("Cannot switch extensions") return False new_context = new["context"] old_context = old["context"] if new_context["asset"] != old_context["asset"]: log.error("Changing assets between updates is " "not supported.") return False if new_context["subset"] != old_context["subset"]: log.error("Changing subsets between updates is " "not supported.") return False return True ================================================ FILE: openpype/hosts/maya/api/shader_definition_editor.py ================================================ # -*- coding: utf-8 -*- """Editor for shader definitions. Shader names are stored as simple text file over GridFS in mongodb. """ import os from qtpy import QtWidgets, QtCore, QtGui from openpype.client.mongo import OpenPypeMongoConnection from openpype import resources import gridfs DEFINITION_FILENAME = "{}/maya/shader_definition.txt".format( os.getenv("AVALON_PROJECT")) class ShaderDefinitionsEditor(QtWidgets.QWidget): """Widget serving as simple editor for shader name definitions.""" # name of the file used to store definitions def __init__(self, parent=None): super(ShaderDefinitionsEditor, self).__init__(parent) self._mongo = OpenPypeMongoConnection.get_mongo_client() self._gridfs = gridfs.GridFS( self._mongo[os.getenv("OPENPYPE_DATABASE_NAME")]) self._editor = None self._original_content = self._read_definition_file() self.setObjectName("shaderDefinitionEditor") self.setWindowTitle("OpenPype shader name definition editor") icon = QtGui.QIcon(resources.get_openpype_icon_filepath()) self.setWindowIcon(icon) self.setWindowFlags(QtCore.Qt.Window) self.setParent(parent) self.setAttribute(QtCore.Qt.WA_DeleteOnClose) self.resize(750, 500) self._setup_ui() self._reload() def _setup_ui(self): """Setup UI of Widget.""" layout = QtWidgets.QVBoxLayout(self) label = QtWidgets.QLabel() label.setText("Put shader names here - one name per line:") layout.addWidget(label) self._editor = QtWidgets.QPlainTextEdit() self._editor.setStyleSheet("border: none;") layout.addWidget(self._editor) btn_layout = QtWidgets.QHBoxLayout() save_btn = QtWidgets.QPushButton("Save") save_btn.clicked.connect(self._save) reload_btn = QtWidgets.QPushButton("Reload") reload_btn.clicked.connect(self._reload) exit_btn = QtWidgets.QPushButton("Exit") exit_btn.clicked.connect(self._close) btn_layout.addWidget(reload_btn) btn_layout.addWidget(save_btn) btn_layout.addWidget(exit_btn) layout.addLayout(btn_layout) def _read_definition_file(self, file=None): """Read definition file from database. Args: file (gridfs.grid_file.GridOut, Optional): File to read. If not set, new query will be issued to find it. Returns: str: Content of the file or empty string if file doesn't exist. """ content = "" if not file: file = self._gridfs.find_one( {"filename": DEFINITION_FILENAME}) if not file: print(">>> [SNDE]: nothing in database yet") return content content = file.read() file.close() return content def _write_definition_file(self, content, force=False): """Write content as definition to file in database. Before file is written, check is made if its content has not changed. If is changed, warning is issued to user if he wants it to overwrite. Note: GridFs doesn't allow changing file content. You need to delete existing file and create new one. Args: content (str): Content to write. Raises: ContentException: If file is changed in database while editor is running. """ file = self._gridfs.find_one( {"filename": DEFINITION_FILENAME}) if file: content_check = self._read_definition_file(file) if content == content_check: print(">>> [SNDE]: content not changed") return if self._original_content != content_check: if not force: raise ContentException("Content changed") print(">>> [SNDE]: overwriting data") file.close() self._gridfs.delete(file._id) file = self._gridfs.new_file( filename=DEFINITION_FILENAME, content_type='text/plain', encoding='utf-8') file.write(content) file.close() QtCore.QTimer.singleShot(200, self._reset_style) self._editor.setStyleSheet("border: 1px solid #33AF65;") self._original_content = content def _reset_style(self): """Reset editor style back. Used to visually indicate save. """ self._editor.setStyleSheet("border: none;") def _close(self): self.hide() def closeEvent(self, event): event.ignore() self.hide() def _reload(self): print(">>> [SNDE]: reloading") self._set_content(self._read_definition_file()) def _save(self): try: self._write_definition_file(content=self._editor.toPlainText()) except ContentException: # content has changed meanwhile print(">>> [SNDE]: content has changed") self._show_overwrite_warning() def _set_content(self, content): self._editor.setPlainText(content) def _show_overwrite_warning(self): reply = QtWidgets.QMessageBox.question( self, "Warning", ("Content you are editing was changed meanwhile in database.\n" "Please, reload and solve the conflict."), QtWidgets.QMessageBox.OK) if reply == QtWidgets.QMessageBox.OK: # do nothing pass class ContentException(Exception): """This is risen during save if file is changed in database.""" pass ================================================ FILE: openpype/hosts/maya/api/workfile_template_builder.py ================================================ import json from maya import cmds from openpype.pipeline import registered_host, get_current_asset_name from openpype.pipeline.workfile.workfile_template_builder import ( TemplateAlreadyImported, AbstractTemplateBuilder, PlaceholderPlugin, LoadPlaceholderItem, PlaceholderLoadMixin, ) from openpype.tools.workfile_template_build import ( WorkfileBuildPlaceholderDialog, ) from .lib import read, imprint, get_reference_node, get_main_window PLACEHOLDER_SET = "PLACEHOLDERS_SET" class MayaTemplateBuilder(AbstractTemplateBuilder): """Concrete implementation of AbstractTemplateBuilder for maya""" use_legacy_creators = True def import_template(self, path): """Import template into current scene. Block if a template is already loaded. Args: path (str): A path to current template (usually given by get_template_preset implementation) Returns: bool: Whether the template was successfully imported or not """ if cmds.objExists(PLACEHOLDER_SET): raise TemplateAlreadyImported(( "Build template already loaded\n" "Clean scene if needed (File > New Scene)" )) cmds.sets(name=PLACEHOLDER_SET, empty=True) new_nodes = cmds.file( path, i=True, returnNewNodes=True, preserveReferences=True, loadReferenceDepth="all", ) # make default cameras non-renderable default_cameras = [cam for cam in cmds.ls(cameras=True) if cmds.camera(cam, query=True, startupCamera=True)] for cam in default_cameras: if not cmds.attributeQuery("renderable", node=cam, exists=True): self.log.debug( "Camera {} has no attribute 'renderable'".format(cam) ) continue cmds.setAttr("{}.renderable".format(cam), 0) cmds.setAttr(PLACEHOLDER_SET + ".hiddenInOutliner", True) imported_sets = cmds.ls(new_nodes, set=True) if not imported_sets: return True # update imported sets information asset_name = get_current_asset_name() for node in imported_sets: if not cmds.attributeQuery("id", node=node, exists=True): continue if cmds.getAttr("{}.id".format(node)) != "pyblish.avalon.instance": continue if not cmds.attributeQuery("asset", node=node, exists=True): continue cmds.setAttr( "{}.asset".format(node), asset_name, type="string") return True class MayaPlaceholderLoadPlugin(PlaceholderPlugin, PlaceholderLoadMixin): identifier = "maya.load" label = "Maya load" def _collect_scene_placeholders(self): # Cache placeholder data to shared data placeholder_nodes = self.builder.get_shared_populate_data( "placeholder_nodes" ) if placeholder_nodes is None: attributes = cmds.ls("*.plugin_identifier", long=True) placeholder_nodes = {} for attribute in attributes: node_name = attribute.rpartition(".")[0] placeholder_nodes[node_name] = ( self._parse_placeholder_node_data(node_name) ) self.builder.set_shared_populate_data( "placeholder_nodes", placeholder_nodes ) return placeholder_nodes def _parse_placeholder_node_data(self, node_name): placeholder_data = read(node_name) parent_name = ( cmds.getAttr(node_name + ".parent", asString=True) or node_name.rpartition("|")[0] or "" ) if parent_name: siblings = cmds.listRelatives(parent_name, children=True) else: siblings = cmds.ls(assemblies=True) node_shortname = node_name.rpartition("|")[2] current_index = cmds.getAttr(node_name + ".index", asString=True) if current_index < 0: current_index = siblings.index(node_shortname) placeholder_data.update({ "parent": parent_name, "index": current_index }) return placeholder_data def _create_placeholder_name(self, placeholder_data): placeholder_name_parts = placeholder_data["builder_type"].split("_") pos = 1 # add family in any placeholder_family = placeholder_data["family"] if placeholder_family: placeholder_name_parts.insert(pos, placeholder_family) pos += 1 # add loader arguments if any loader_args = placeholder_data["loader_args"] if loader_args: loader_args = json.loads(loader_args.replace('\'', '\"')) values = [v for v in loader_args.values()] for value in values: placeholder_name_parts.insert(pos, value) pos += 1 placeholder_name = "_".join(placeholder_name_parts) return placeholder_name.capitalize() def _get_loaded_repre_ids(self): loaded_representation_ids = self.builder.get_shared_populate_data( "loaded_representation_ids" ) if loaded_representation_ids is None: try: containers = cmds.sets("AVALON_CONTAINERS", q=True) except ValueError: containers = [] loaded_representation_ids = { cmds.getAttr(container + ".representation") for container in containers } self.builder.set_shared_populate_data( "loaded_representation_ids", loaded_representation_ids ) return loaded_representation_ids def create_placeholder(self, placeholder_data): selection = cmds.ls(selection=True) if len(selection) > 1: raise ValueError("More then one item are selected") parent = selection[0] if selection else None placeholder_data["plugin_identifier"] = self.identifier placeholder_name = self._create_placeholder_name(placeholder_data) placeholder = cmds.spaceLocator(name=placeholder_name)[0] if parent: placeholder = cmds.parent(placeholder, selection[0])[0] imprint(placeholder, placeholder_data) # Add helper attributes to keep placeholder info cmds.addAttr( placeholder, longName="parent", hidden=True, dataType="string" ) cmds.addAttr( placeholder, longName="index", hidden=True, attributeType="short", defaultValue=-1 ) cmds.setAttr(placeholder + ".parent", "", type="string") def update_placeholder(self, placeholder_item, placeholder_data): node_name = placeholder_item.scene_identifier new_values = {} for key, value in placeholder_data.items(): placeholder_value = placeholder_item.data.get(key) if value != placeholder_value: new_values[key] = value placeholder_item.data[key] = value for key in new_values.keys(): cmds.deleteAttr(node_name + "." + key) imprint(node_name, new_values) def collect_placeholders(self): output = [] scene_placeholders = self._collect_scene_placeholders() for node_name, placeholder_data in scene_placeholders.items(): if placeholder_data.get("plugin_identifier") != self.identifier: continue # TODO do data validations and maybe upgrades if they are invalid output.append( LoadPlaceholderItem(node_name, placeholder_data, self) ) return output def populate_placeholder(self, placeholder): self.populate_load_placeholder(placeholder) def repopulate_placeholder(self, placeholder): repre_ids = self._get_loaded_repre_ids() self.populate_load_placeholder(placeholder, repre_ids) def get_placeholder_options(self, options=None): return self.get_load_plugin_options(options) def post_placeholder_process(self, placeholder, failed): """Cleanup placeholder after load of its corresponding representations. Args: placeholder (PlaceholderItem): Item which was just used to load representation. failed (bool): Loading of representation failed. """ # Hide placeholder and add them to placeholder set node = placeholder.scene_identifier cmds.sets(node, addElement=PLACEHOLDER_SET) cmds.hide(node) cmds.setAttr(node + ".hiddenInOutliner", True) def delete_placeholder(self, placeholder): """Remove placeholder if building was successful""" cmds.delete(placeholder.scene_identifier) def load_succeed(self, placeholder, container): self._parent_in_hierarchy(placeholder, container) def _parent_in_hierarchy(self, placeholder, container): """Parent loaded container to placeholder's parent. ie : Set loaded content as placeholder's sibling Args: container (str): Placeholder loaded containers """ if not container: return roots = cmds.sets(container, q=True) ref_node = None try: ref_node = get_reference_node(roots) except AssertionError as e: self.log.info(e.args[0]) nodes_to_parent = [] for root in roots: if ref_node: ref_root = cmds.referenceQuery(root, nodes=True)[0] ref_root = ( cmds.listRelatives(ref_root, parent=True, path=True) or [ref_root] ) nodes_to_parent.extend(ref_root) continue if root.endswith("_RN"): # Backwards compatibility for hardcoded reference names. refRoot = cmds.referenceQuery(root, n=True)[0] refRoot = cmds.listRelatives(refRoot, parent=True) or [refRoot] nodes_to_parent.extend(refRoot) elif root not in cmds.listSets(allSets=True): nodes_to_parent.append(root) elif not cmds.sets(root, q=True): return # Move loaded nodes to correct index in outliner hierarchy placeholder_form = cmds.xform( placeholder.scene_identifier, q=True, matrix=True, worldSpace=True ) scene_parent = cmds.listRelatives( placeholder.scene_identifier, parent=True, fullPath=True ) for node in set(nodes_to_parent): cmds.reorder(node, front=True) cmds.reorder(node, relative=placeholder.data["index"]) cmds.xform(node, matrix=placeholder_form, ws=True) if scene_parent: cmds.parent(node, scene_parent) else: cmds.parent(node, world=True) holding_sets = cmds.listSets(object=placeholder.scene_identifier) if not holding_sets: return for holding_set in holding_sets: cmds.sets(roots, forceElement=holding_set) def build_workfile_template(*args): builder = MayaTemplateBuilder(registered_host()) builder.build_template() def update_workfile_template(*args): builder = MayaTemplateBuilder(registered_host()) builder.rebuild_template() def create_placeholder(*args): host = registered_host() builder = MayaTemplateBuilder(host) window = WorkfileBuildPlaceholderDialog(host, builder, parent=get_main_window()) window.show() def update_placeholder(*args): host = registered_host() builder = MayaTemplateBuilder(host) placeholder_items_by_id = { placeholder_item.scene_identifier: placeholder_item for placeholder_item in builder.get_placeholders() } placeholder_items = [] for node_name in cmds.ls(selection=True, long=True): if node_name in placeholder_items_by_id: placeholder_items.append(placeholder_items_by_id[node_name]) # TODO show UI at least if len(placeholder_items) == 0: raise ValueError("No node selected") if len(placeholder_items) > 1: raise ValueError("Too many selected nodes") placeholder_item = placeholder_items[0] window = WorkfileBuildPlaceholderDialog(host, builder, parent=get_main_window()) window.set_update_mode(placeholder_item) window.exec_() ================================================ FILE: openpype/hosts/maya/api/workio.py ================================================ """Host API required Work Files tool""" import os from maya import cmds def file_extensions(): return [".ma", ".mb"] def has_unsaved_changes(): return cmds.file(query=True, modified=True) def save_file(filepath): cmds.file(rename=filepath) ext = os.path.splitext(filepath)[1] if ext == ".mb": file_type = "mayaBinary" else: file_type = "mayaAscii" cmds.file(save=True, type=file_type) def open_file(filepath): return cmds.file(filepath, open=True, force=True) def current_file(): current_filepath = cmds.file(query=True, sceneName=True) if not current_filepath: return None return current_filepath def work_root(session): work_dir = session["AVALON_WORKDIR"] scene_dir = None # Query scene file rule from workspace.mel if it exists in WORKDIR # We are parsing the workspace.mel manually as opposed to temporarily # setting the Workspace in Maya in a context manager since Maya had a # tendency to crash on frequently changing the workspace when this # function was called many times as one scrolled through Work Files assets. workspace_mel = os.path.join(work_dir, "workspace.mel") if os.path.exists(workspace_mel): scene_rule = 'workspace -fr "scene" ' # We need to use builtins as `open` is overridden by the workio API open_file = __builtins__["open"] with open_file(workspace_mel, "r") as f: for line in f: if line.strip().startswith(scene_rule): # remainder == "rule"; remainder = line[len(scene_rule):] # scene_dir == rule scene_dir = remainder.split('"')[1] else: # We can't query a workspace that does not exist # so we return similar to what we do in other hosts. scene_dir = session.get("AVALON_SCENEDIR") if scene_dir: return os.path.join(work_dir, scene_dir) else: return work_dir ================================================ FILE: openpype/hosts/maya/hooks/pre_auto_load_plugins.py ================================================ from openpype.lib.applications import PreLaunchHook, LaunchTypes class MayaPreAutoLoadPlugins(PreLaunchHook): """Define -noAutoloadPlugins command flag.""" # Before AddLastWorkfileToLaunchArgs order = 9 app_groups = {"maya"} launch_types = {LaunchTypes.local} def execute(self): # Ignore if there's no last workfile to start. if not self.data.get("start_last_workfile"): return maya_settings = self.data["project_settings"]["maya"] enabled = maya_settings["explicit_plugins_loading"]["enabled"] if enabled: # Force disable the `AddLastWorkfileToLaunchArgs`. self.data.pop("start_last_workfile") # Force post initialization so our dedicated plug-in load can run # prior to Maya opening a scene file. key = "OPENPYPE_OPEN_WORKFILE_POST_INITIALIZATION" self.launch_context.env[key] = "1" self.log.debug("Explicit plugins loading.") self.launch_context.launch_args.append("-noAutoloadPlugins") ================================================ FILE: openpype/hosts/maya/hooks/pre_copy_mel.py ================================================ from openpype.lib.applications import PreLaunchHook, LaunchTypes from openpype.hosts.maya.lib import create_workspace_mel class PreCopyMel(PreLaunchHook): """Copy workspace.mel to workdir. Hook `GlobalHostDataHook` must be executed before this hook. """ app_groups = {"maya", "mayapy"} launch_types = {LaunchTypes.local} def execute(self): project_doc = self.data["project_doc"] workdir = self.launch_context.env.get("AVALON_WORKDIR") if not workdir: self.log.warning("BUG: Workdir is not filled.") return project_settings = self.data["project_settings"] create_workspace_mel(workdir, project_doc["name"], project_settings) ================================================ FILE: openpype/hosts/maya/hooks/pre_open_workfile_post_initialization.py ================================================ from openpype.lib.applications import PreLaunchHook, LaunchTypes class MayaPreOpenWorkfilePostInitialization(PreLaunchHook): """Define whether open last workfile should run post initialize.""" # Before AddLastWorkfileToLaunchArgs. order = 9 app_groups = {"maya"} launch_types = {LaunchTypes.local} def execute(self): # Ignore if there's no last workfile to start. if not self.data.get("start_last_workfile"): return maya_settings = self.data["project_settings"]["maya"] enabled = maya_settings["open_workfile_post_initialization"] if enabled: # Force disable the `AddLastWorkfileToLaunchArgs`. self.data.pop("start_last_workfile") self.log.debug("Opening workfile post initialization.") key = "OPENPYPE_OPEN_WORKFILE_POST_INITIALIZATION" self.launch_context.env[key] = "1" ================================================ FILE: openpype/hosts/maya/lib.py ================================================ import os from openpype.settings import get_project_settings from openpype.lib import Logger def create_workspace_mel(workdir, project_name, project_settings=None): dst_filepath = os.path.join(workdir, "workspace.mel") if os.path.exists(dst_filepath): return if not os.path.exists(workdir): os.makedirs(workdir) if not project_settings: project_settings = get_project_settings(project_name) mel_script = project_settings["maya"].get("mel_workspace") # Skip if mel script in settings is empty if not mel_script: log = Logger.get_logger("create_workspace_mel") log.debug("File 'workspace.mel' not created. Settings value is empty.") return with open(dst_filepath, "w") as mel_file: mel_file.write(mel_script) ================================================ FILE: openpype/hosts/maya/plugins/__init__.py ================================================ ================================================ FILE: openpype/hosts/maya/plugins/create/convert_legacy.py ================================================ from openpype.pipeline.create.creator_plugins import SubsetConvertorPlugin from openpype.hosts.maya.api import plugin from openpype.hosts.maya.api.lib import read from openpype.client import get_asset_by_name from maya import cmds from maya.app.renderSetup.model import renderSetup class MayaLegacyConvertor(SubsetConvertorPlugin, plugin.MayaCreatorBase): """Find and convert any legacy subsets in the scene. This Convertor will find all legacy subsets in the scene and will transform them to the current system. Since the old subsets doesn't retain any information about their original creators, the only mapping we can do is based on their families. Its limitation is that you can have multiple creators creating subset of the same family and there is no way to handle it. This code should nevertheless cover all creators that came with OpenPype. """ identifier = "io.openpype.creators.maya.legacy" # Cases where the identifier or new family doesn't correspond to the # original family on the legacy instances special_family_conversions = { "rendering": "io.openpype.creators.maya.renderlayer", } def find_instances(self): self.cache_subsets(self.collection_shared_data) legacy = self.collection_shared_data.get("maya_cached_legacy_subsets") if not legacy: return self.add_convertor_item("Convert legacy instances") def convert(self): self.remove_convertor_item() # We can't use the collected shared data cache here # we re-query it here directly to convert all found. cache = {} self.cache_subsets(cache) legacy = cache.get("maya_cached_legacy_subsets") if not legacy: return # From all current new style manual creators find the mapping # from family to identifier family_to_id = {} for identifier, creator in self.create_context.creators.items(): family = getattr(creator, "family", None) if not family: continue if family in family_to_id: # We have a clash of family -> identifier. Multiple # new style creators use the same family self.log.warning("Clash on family->identifier: " "{}".format(identifier)) family_to_id[family] = identifier family_to_id.update(self.special_family_conversions) # We also embed the current 'task' into the instance since legacy # instances didn't store that data on the instances. The old style # logic was thus to be live to the current task to begin with. data = dict() data["task"] = self.create_context.get_current_task_name() for family, instance_nodes in legacy.items(): if family not in family_to_id: self.log.warning( "Unable to convert legacy instance with family '{}'" " because there is no matching new creator's family" "".format(family) ) continue creator_id = family_to_id[family] creator = self.create_context.creators[creator_id] data["creator_identifier"] = creator_id if isinstance(creator, plugin.RenderlayerCreator): self._convert_per_renderlayer(instance_nodes, data, creator) else: self._convert_regular(instance_nodes, data) def _convert_regular(self, instance_nodes, data): # We only imprint the creator identifier for it to identify # as the new style creator for instance_node in instance_nodes: self.imprint_instance_node(instance_node, data=data.copy()) def _convert_per_renderlayer(self, instance_nodes, data, creator): # Split the instance into an instance per layer rs = renderSetup.instance() layers = rs.getRenderLayers() if not layers: self.log.error( "Can't convert legacy renderlayer instance because no existing" " renderSetup layers exist in the scene." ) return creator_attribute_names = { attr_def.key for attr_def in creator.get_instance_attr_defs() } for instance_node in instance_nodes: # Ensure we have the new style singleton node generated # TODO: Make function public singleton_node = creator._get_singleton_node() if singleton_node: self.log.error( "Can't convert legacy renderlayer instance '{}' because" " new style instance '{}' already exists".format( instance_node, singleton_node ) ) continue creator.create_singleton_node() # We are creating new nodes to replace the original instance # Copy the attributes of the original instance to the new node original_data = read(instance_node) # The family gets converted to the new family (this is due to # "rendering" family being converted to "renderlayer" family) original_data["family"] = creator.family # recreate subset name as without it would be # `renderingMain` vs correct `renderMain` project_name = self.create_context.get_current_project_name() asset_doc = get_asset_by_name(project_name, original_data["asset"]) subset_name = creator.get_subset_name( original_data["variant"], data["task"], asset_doc, project_name) original_data["subset"] = subset_name # Convert to creator attributes when relevant creator_attributes = {} for key in list(original_data.keys()): # Iterate in order of the original attributes to preserve order # in the output creator attributes if key in creator_attribute_names: creator_attributes[key] = original_data.pop(key) original_data["creator_attributes"] = creator_attributes # For layer in maya layers for layer in layers: layer_instance_node = creator.find_layer_instance_node(layer) if not layer_instance_node: # TODO: Make function public layer_instance_node = creator._create_layer_instance_node( layer ) # Transfer the main attributes of the original instance layer_data = original_data.copy() layer_data.update(data) self.imprint_instance_node(layer_instance_node, data=layer_data) # Delete the legacy instance node cmds.delete(instance_node) ================================================ FILE: openpype/hosts/maya/plugins/create/create_animation_pointcache.py ================================================ from maya import cmds from openpype.hosts.maya.api import lib, plugin from openpype.lib import ( BoolDef, NumberDef, ) from openpype.pipeline import CreatedInstance def _get_animation_attr_defs(cls): """Get Animation generic definitions.""" defs = lib.collect_animation_defs() defs.extend( [ BoolDef("farm", label="Submit to Farm"), NumberDef("priority", label="Farm job Priority", default=50), BoolDef("refresh", label="Refresh viewport during export"), BoolDef( "includeParentHierarchy", label="Include Parent Hierarchy" ), BoolDef( "includeUserDefinedAttributes", label="Include User Defined Attributes" ), ] ) return defs def extract_alembic_attributes(node_data, class_name): """This is a legacy transfer of creator attributes to publish attributes for ExtractAlembic/ExtractAnimation plugin. """ publish_attributes = node_data["publish_attributes"] if class_name in publish_attributes: return node_data extract_alembic_flags = [ "writeColorSets", "writeFaceSets", "writeNormals", "renderableOnly", "visibleOnly", "worldSpace", "renderableOnly" ] extract_alembic_attributes = [ "attr", "attrPrefix", "visibleOnly" ] attributes = extract_alembic_flags + extract_alembic_attributes plugin_attributes = {"flags": []} for attr in attributes: if attr not in node_data["creator_attributes"].keys(): continue value = node_data["creator_attributes"].pop(attr) if value and attr in extract_alembic_flags: plugin_attributes["flags"].append(attr) if attr in extract_alembic_attributes: plugin_attributes[attr] = value publish_attributes[class_name] = plugin_attributes return node_data class CreateAnimation(plugin.MayaHiddenCreator): """Animation output for character rigs We hide the animation creator from the UI since the creation of it is automated upon loading a rig. There's an inventory action to recreate it for loaded rigs if by chance someone deleted the animation instance. """ identifier = "io.openpype.creators.maya.animation" name = "animationDefault" label = "Animation" family = "animation" icon = "male" write_color_sets = False write_face_sets = False include_parent_hierarchy = False include_user_defined_attributes = False def collect_instances(self): try: cached_subsets = self.collection_shared_data["maya_cached_subsets"] except KeyError: self.cache_subsets(self.collection_shared_data) cached_subsets = self.collection_shared_data["maya_cached_subsets"] for node in cached_subsets.get(self.identifier, []): node_data = self.read_instance_node(node) node_data = extract_alembic_attributes( node_data, "ExtractAnimation" ) created_instance = CreatedInstance.from_existing(node_data, self) self._add_instance_to_context(created_instance) def get_instance_attr_defs(self): super(CreateAnimation, self).get_instance_attr_defs() defs = _get_animation_attr_defs(self) return defs class CreatePointCache(plugin.MayaCreator): """Alembic pointcache for animated data""" identifier = "io.openpype.creators.maya.pointcache" label = "Pointcache" family = "pointcache" icon = "gears" write_color_sets = False write_face_sets = False include_user_defined_attributes = False def collect_instances(self): try: cached_subsets = self.collection_shared_data["maya_cached_subsets"] except KeyError: self.cache_subsets(self.collection_shared_data) cached_subsets = self.collection_shared_data["maya_cached_subsets"] for node in cached_subsets.get(self.identifier, []): node_data = self.read_instance_node(node) node_data = extract_alembic_attributes(node_data, "ExtractAlembic") created_instance = CreatedInstance.from_existing(node_data, self) self._add_instance_to_context(created_instance) def get_instance_attr_defs(self): super(CreatePointCache, self).get_instance_attr_defs() defs = _get_animation_attr_defs(self) return defs def create(self, subset_name, instance_data, pre_create_data): instance = super(CreatePointCache, self).create( subset_name, instance_data, pre_create_data ) instance_node = instance.get("instance_node") # For Arnold standin proxy proxy_set = cmds.sets(name=instance_node + "_proxy_SET", empty=True) cmds.sets(proxy_set, forceElement=instance_node) ================================================ FILE: openpype/hosts/maya/plugins/create/create_arnold_scene_source.py ================================================ from maya import cmds from openpype.hosts.maya.api import ( lib, plugin ) from openpype.lib import ( NumberDef, BoolDef ) class CreateArnoldSceneSource(plugin.MayaCreator): """Arnold Scene Source""" identifier = "io.openpype.creators.maya.ass" label = "Arnold Scene Source" family = "ass" icon = "cube" settings_name = "CreateAss" expandProcedurals = False motionBlur = True motionBlurKeys = 2 motionBlurLength = 0.5 maskOptions = False maskCamera = False maskLight = False maskShape = False maskShader = False maskOverride = False maskDriver = False maskFilter = False maskColor_manager = False maskOperator = False def get_instance_attr_defs(self): defs = lib.collect_animation_defs() defs.extend([ BoolDef("expandProcedural", label="Expand Procedural", default=self.expandProcedurals), BoolDef("motionBlur", label="Motion Blur", default=self.motionBlur), NumberDef("motionBlurKeys", label="Motion Blur Keys", decimals=0, default=self.motionBlurKeys), NumberDef("motionBlurLength", label="Motion Blur Length", decimals=3, default=self.motionBlurLength), # Masks BoolDef("maskOptions", label="Export Options", default=self.maskOptions), BoolDef("maskCamera", label="Export Cameras", default=self.maskCamera), BoolDef("maskLight", label="Export Lights", default=self.maskLight), BoolDef("maskShape", label="Export Shapes", default=self.maskShape), BoolDef("maskShader", label="Export Shaders", default=self.maskShader), BoolDef("maskOverride", label="Export Override Nodes", default=self.maskOverride), BoolDef("maskDriver", label="Export Drivers", default=self.maskDriver), BoolDef("maskFilter", label="Export Filters", default=self.maskFilter), BoolDef("maskOperator", label="Export Operators", default=self.maskOperator), BoolDef("maskColor_manager", label="Export Color Managers", default=self.maskColor_manager), ]) return defs class CreateArnoldSceneSourceProxy(CreateArnoldSceneSource): """Arnold Scene Source Proxy This product type facilitates working with proxy geometry in the viewport. """ identifier = "io.openpype.creators.maya.assproxy" label = "Arnold Scene Source Proxy" family = "assProxy" icon = "cube" def create(self, subset_name, instance_data, pre_create_data): instance = super(CreateArnoldSceneSource, self).create( subset_name, instance_data, pre_create_data ) instance_node = instance.get("instance_node") proxy = cmds.sets(name=instance_node + "_proxy_SET", empty=True) cmds.sets([proxy], forceElement=instance_node) ================================================ FILE: openpype/hosts/maya/plugins/create/create_assembly.py ================================================ from openpype.hosts.maya.api import plugin class CreateAssembly(plugin.MayaCreator): """A grouped package of loaded content""" identifier = "io.openpype.creators.maya.assembly" label = "Assembly" family = "assembly" icon = "cubes" ================================================ FILE: openpype/hosts/maya/plugins/create/create_camera.py ================================================ from openpype.hosts.maya.api import ( lib, plugin ) from openpype.lib import BoolDef class CreateCamera(plugin.MayaCreator): """Single baked camera""" identifier = "io.openpype.creators.maya.camera" label = "Camera" family = "camera" icon = "video-camera" def get_instance_attr_defs(self): defs = lib.collect_animation_defs() defs.extend([ BoolDef("bakeToWorldSpace", label="Bake to World-Space", tooltip="Bake to World-Space", default=True), ]) return defs class CreateCameraRig(plugin.MayaCreator): """Complex hierarchy with camera.""" identifier = "io.openpype.creators.maya.camerarig" label = "Camera Rig" family = "camerarig" icon = "video-camera" ================================================ FILE: openpype/hosts/maya/plugins/create/create_layout.py ================================================ from openpype.hosts.maya.api import plugin from openpype.lib import BoolDef class CreateLayout(plugin.MayaCreator): """A grouped package of loaded content""" identifier = "io.openpype.creators.maya.layout" label = "Layout" family = "layout" icon = "cubes" def get_instance_attr_defs(self): return [ BoolDef("groupLoadedAssets", label="Group Loaded Assets", tooltip="Enable this when you want to publish group of " "loaded asset", default=False) ] ================================================ FILE: openpype/hosts/maya/plugins/create/create_look.py ================================================ from openpype.hosts.maya.api import ( plugin, lib ) from openpype.lib import ( BoolDef, TextDef ) class CreateLook(plugin.MayaCreator): """Shader connections defining shape look""" identifier = "io.openpype.creators.maya.look" label = "Look" family = "look" icon = "paint-brush" make_tx = True rs_tex = False def get_instance_attr_defs(self): return [ # TODO: This value should actually get set on create! TextDef("renderLayer", # TODO: Bug: Hidden attribute's label is still shown in UI? hidden=True, default=lib.get_current_renderlayer(), label="Renderlayer", tooltip="Renderlayer to extract the look from"), BoolDef("maketx", label="MakeTX", tooltip="Whether to generate .tx files for your textures", default=self.make_tx), BoolDef("rstex", label="Convert textures to .rstex", tooltip="Whether to generate Redshift .rstex files for " "your textures", default=self.rs_tex) ] def get_pre_create_attr_defs(self): # Show same attributes on create but include use selection defs = super(CreateLook, self).get_pre_create_attr_defs() defs.extend(self.get_instance_attr_defs()) return defs ================================================ FILE: openpype/hosts/maya/plugins/create/create_matchmove.py ================================================ from openpype.hosts.maya.api import ( lib, plugin ) from openpype.lib import BoolDef class CreateMatchmove(plugin.MayaCreator): """Instance for more complex setup of cameras. Might contain multiple cameras, geometries etc. It is expected to be extracted into .abc or .ma """ identifier = "io.openpype.creators.maya.matchmove" label = "Matchmove" family = "matchmove" icon = "video-camera" def get_instance_attr_defs(self): defs = lib.collect_animation_defs() defs.extend([ BoolDef("bakeToWorldSpace", label="Bake Cameras to World-Space", tooltip="Bake Cameras to World-Space", default=True), ]) return defs ================================================ FILE: openpype/hosts/maya/plugins/create/create_maya_usd.py ================================================ from openpype.hosts.maya.api import plugin, lib from openpype.lib import ( BoolDef, EnumDef, TextDef ) from maya import cmds class CreateMayaUsd(plugin.MayaCreator): """Create Maya USD Export""" identifier = "io.openpype.creators.maya.mayausd" label = "Maya USD" family = "usd" icon = "cubes" description = "Create Maya USD Export" cache = {} def get_publish_families(self): return ["usd", "mayaUsd"] def get_instance_attr_defs(self): if "jobContextItems" not in self.cache: # Query once instead of per instance job_context_items = {} try: cmds.loadPlugin("mayaUsdPlugin", quiet=True) job_context_items = { cmds.mayaUSDListJobContexts(jobContext=name): name for name in cmds.mayaUSDListJobContexts(export=True) or [] } except RuntimeError: # Likely `mayaUsdPlugin` plug-in not available self.log.warning("Unable to retrieve available job " "contexts for `mayaUsdPlugin` exports") if not job_context_items: # enumdef multiselection may not be empty job_context_items = [" "] self.cache["jobContextItems"] = job_context_items defs = lib.collect_animation_defs() defs.extend([ EnumDef("defaultUSDFormat", label="File format", items={ "usdc": "Binary", "usda": "ASCII" }, default="usdc"), BoolDef("stripNamespaces", label="Strip Namespaces", tooltip=( "Remove namespaces during export. By default, " "namespaces are exported to the USD file in the " "following format: nameSpaceExample_pPlatonic1" ), default=True), BoolDef("mergeTransformAndShape", label="Merge Transform and Shape", tooltip=( "Combine Maya transform and shape into a single USD" "prim that has transform and geometry, for all" " \"geometric primitives\" (gprims).\n" "This results in smaller and faster scenes. Gprims " "will be \"unpacked\" back into transform and shape " "nodes when imported into Maya from USD." ), default=True), BoolDef("includeUserDefinedAttributes", label="Include User Defined Attributes", tooltip=( "Whether to include all custom maya attributes found " "on nodes as metadata (userProperties) in USD." ), default=False), TextDef("attr", label="Custom Attributes", default="", placeholder="attr1, attr2"), TextDef("attrPrefix", label="Custom Attributes Prefix", default="", placeholder="prefix1, prefix2"), EnumDef("jobContext", label="Job Context", items=self.cache["jobContextItems"], tooltip=( "Specifies an additional export context to handle.\n" "These usually contain extra schemas, primitives,\n" "and materials that are to be exported for a " "specific\ntask, a target renderer for example." ), multiselection=True), ]) return defs ================================================ FILE: openpype/hosts/maya/plugins/create/create_mayascene.py ================================================ from openpype.hosts.maya.api import plugin class CreateMayaScene(plugin.MayaCreator): """Raw Maya Scene file export""" identifier = "io.openpype.creators.maya.mayascene" name = "mayaScene" label = "Maya Scene" family = "mayaScene" icon = "file-archive-o" ================================================ FILE: openpype/hosts/maya/plugins/create/create_model.py ================================================ from openpype.hosts.maya.api import plugin from openpype.lib import ( BoolDef, TextDef ) class CreateModel(plugin.MayaCreator): """Polygonal static geometry""" identifier = "io.openpype.creators.maya.model" label = "Model" family = "model" icon = "cube" default_variants = ["Main", "Proxy", "_MD", "_HD", "_LD"] write_color_sets = False write_face_sets = False def get_instance_attr_defs(self): return [ BoolDef("writeColorSets", label="Write vertex colors", tooltip="Write vertex colors with the geometry", default=self.write_color_sets), BoolDef("writeFaceSets", label="Write face sets", tooltip="Write face sets with the geometry", default=self.write_face_sets), BoolDef("includeParentHierarchy", label="Include Parent Hierarchy", tooltip="Whether to include parent hierarchy of nodes in " "the publish instance", default=False), TextDef("attr", label="Custom Attributes", default="", placeholder="attr1, attr2"), TextDef("attrPrefix", label="Custom Attributes Prefix", placeholder="prefix1, prefix2") ] ================================================ FILE: openpype/hosts/maya/plugins/create/create_multishot_layout.py ================================================ from ayon_api import ( get_folder_by_name, get_folder_by_path, get_folders, ) from maya import cmds # noqa: F401 from openpype import AYON_SERVER_ENABLED from openpype.client import get_assets from openpype.hosts.maya.api import plugin from openpype.lib import BoolDef, EnumDef, TextDef from openpype.pipeline import ( Creator, get_current_asset_name, get_current_project_name, ) from openpype.pipeline.create import CreatorError class CreateMultishotLayout(plugin.MayaCreator): """Create a multi-shot layout in the Maya scene. This creator will create a Camera Sequencer in the Maya scene based on the shots found under the specified folder. The shots will be added to the sequencer in the order of their clipIn and clipOut values. For each shot a Layout will be created. """ identifier = "io.openpype.creators.maya.multishotlayout" label = "Multi-shot Layout" family = "layout" icon = "project-diagram" def get_pre_create_attr_defs(self): # Present artist with a list of parents of the current context # to choose from. This will be used to get the shots under the # selected folder to create the Camera Sequencer. """ Todo: `get_folder_by_name` should be switched to `get_folder_by_path` once the fork to pure AYON is done. Warning: this will not work for projects where the asset name is not unique across the project until the switch mentioned above is done. """ project_name = get_current_project_name() folder_path = get_current_asset_name() if "/" in folder_path: current_folder = get_folder_by_path(project_name, folder_path) else: current_folder = get_folder_by_name( project_name, folder_name=folder_path ) current_path_parts = current_folder["path"].split("/") # populate the list with parents of the current folder # this will create menu items like: # [ # { # "value": "", # "label": "project (shots directly under the project)" # }, { # "value": "shots/shot_01", "label": "shot_01 (current)" # }, { # "value": "shots", "label": "shots" # } # ] # add the project as the first item items_with_label = [ { "label": f"{self.project_name} " "(shots directly under the project)", "value": "" } ] # go through the current folder path and add each part to the list, # but mark the current folder. for part_idx, part in enumerate(current_path_parts): label = part if label == current_folder["name"]: label = f"{label} (current)" value = "/".join(current_path_parts[:part_idx + 1]) items_with_label.append({"label": label, "value": value}) return [ EnumDef("shotParent", default=current_folder["name"], label="Shot Parent Folder", items=items_with_label, ), BoolDef("groupLoadedAssets", label="Group Loaded Assets", tooltip="Enable this when you want to publish group of " "loaded asset", default=False), TextDef("taskName", label="Associated Task Name", tooltip=("Task name to be associated " "with the created Layout"), default="layout"), ] def create(self, subset_name, instance_data, pre_create_data): shots = list( self.get_related_shots(folder_path=pre_create_data["shotParent"]) ) if not shots: # There are no shot folders under the specified folder. # We are raising an error here but in the future we might # want to create a new shot folders by publishing the layouts # and shot defined in the sequencer. Sort of editorial publish # in side of Maya. raise CreatorError(( "No shots found under the specified " f"folder: {pre_create_data['shotParent']}.")) # Get layout creator layout_creator_id = "io.openpype.creators.maya.layout" layout_creator: Creator = self.create_context.creators.get( layout_creator_id) if not layout_creator: raise CreatorError( f"Creator {layout_creator_id} not found.") # Get OpenPype style asset documents for the shots op_asset_docs = get_assets( self.project_name, [s["id"] for s in shots]) asset_docs_by_id = {doc["_id"]: doc for doc in op_asset_docs} for shot in shots: # we are setting shot name to be displayed in the sequencer to # `shot name (shot label)` if the label is set, otherwise just # `shot name`. So far, labels are used only when the name is set # with characters that are not allowed in the shot name. if not shot["active"]: continue # get task for shot asset_doc = asset_docs_by_id[shot["id"]] tasks = asset_doc.get("data").get("tasks").keys() layout_task = None if pre_create_data["taskName"] in tasks: layout_task = pre_create_data["taskName"] shot_name = f"{shot['name']}%s" % ( f" ({shot['label']})" if shot["label"] else "") cmds.shot(sequenceStartTime=shot["attrib"]["clipIn"], sequenceEndTime=shot["attrib"]["clipOut"], shotName=shot_name) # Create layout instance by the layout creator instance_data = { "folderPath": shot["path"], "variant": layout_creator.get_default_variant() } if layout_task: instance_data["task"] = layout_task layout_creator.create( subset_name=layout_creator.get_subset_name( layout_creator.get_default_variant(), self.create_context.get_current_task_name(), asset_doc, self.project_name), instance_data=instance_data, pre_create_data={ "groupLoadedAssets": pre_create_data["groupLoadedAssets"] } ) def get_related_shots(self, folder_path: str): """Get all shots related to the current asset. Get all folders of type Shot under specified folder. Args: folder_path (str): Path of the folder. Returns: list: List of dicts with folder data. """ # if folder_path is None, project is selected as a root # and its name is used as a parent id parent_id = self.project_name if folder_path: current_folder = get_folder_by_path( project_name=self.project_name, folder_path=folder_path, ) parent_id = current_folder["id"] # get all child folders of the current one return get_folders( project_name=self.project_name, parent_ids=[parent_id], fields=[ "attrib.clipIn", "attrib.clipOut", "attrib.frameStart", "attrib.frameEnd", "name", "label", "path", "folderType", "id" ] ) # blast this creator if Ayon server is not enabled if not AYON_SERVER_ENABLED: del CreateMultishotLayout ================================================ FILE: openpype/hosts/maya/plugins/create/create_multiverse_look.py ================================================ from openpype.hosts.maya.api import plugin from openpype.lib import ( BoolDef, EnumDef ) class CreateMultiverseLook(plugin.MayaCreator): """Create Multiverse Look""" identifier = "io.openpype.creators.maya.mvlook" label = "Multiverse Look" family = "mvLook" icon = "cubes" def get_instance_attr_defs(self): return [ EnumDef("fileFormat", label="File Format", tooltip="USD export file format", items=["usda", "usd"], default="usda"), BoolDef("publishMipMap", label="Publish MipMap", default=True), ] ================================================ FILE: openpype/hosts/maya/plugins/create/create_multiverse_usd.py ================================================ from openpype.hosts.maya.api import plugin, lib from openpype.lib import ( BoolDef, NumberDef, TextDef, EnumDef ) class CreateMultiverseUsd(plugin.MayaCreator): """Create Multiverse USD Asset""" identifier = "io.openpype.creators.maya.mvusdasset" label = "Multiverse USD Asset" family = "usd" icon = "cubes" description = "Create Multiverse USD Asset" def get_publish_families(self): return ["usd", "mvUsd"] def get_instance_attr_defs(self): defs = lib.collect_animation_defs(fps=True) defs.extend([ EnumDef("fileFormat", label="File format", items=["usd", "usda", "usdz"], default="usd"), BoolDef("stripNamespaces", label="Strip Namespaces", default=True), BoolDef("mergeTransformAndShape", label="Merge Transform and Shape", default=False), BoolDef("writeAncestors", label="Write Ancestors", default=True), BoolDef("flattenParentXforms", label="Flatten Parent Xforms", default=False), BoolDef("writeSparseOverrides", label="Write Sparse Overrides", default=False), BoolDef("useMetaPrimPath", label="Use Meta Prim Path", default=False), TextDef("customRootPath", label="Custom Root Path", default=''), TextDef("customAttributes", label="Custom Attributes", tooltip="Comma-separated list of attribute names", default=''), TextDef("nodeTypesToIgnore", label="Node Types to Ignore", tooltip="Comma-separated list of node types to be ignored", default=''), BoolDef("writeMeshes", label="Write Meshes", default=True), BoolDef("writeCurves", label="Write Curves", default=True), BoolDef("writeParticles", label="Write Particles", default=True), BoolDef("writeCameras", label="Write Cameras", default=False), BoolDef("writeLights", label="Write Lights", default=False), BoolDef("writeJoints", label="Write Joints", default=False), BoolDef("writeCollections", label="Write Collections", default=False), BoolDef("writePositions", label="Write Positions", default=True), BoolDef("writeNormals", label="Write Normals", default=True), BoolDef("writeUVs", label="Write UVs", default=True), BoolDef("writeColorSets", label="Write Color Sets", default=False), BoolDef("writeTangents", label="Write Tangents", default=False), BoolDef("writeRefPositions", label="Write Ref Positions", default=True), BoolDef("writeBlendShapes", label="Write BlendShapes", default=False), BoolDef("writeDisplayColor", label="Write Display Color", default=True), BoolDef("writeSkinWeights", label="Write Skin Weights", default=False), BoolDef("writeMaterialAssignment", label="Write Material Assignment", default=False), BoolDef("writeHardwareShader", label="Write Hardware Shader", default=False), BoolDef("writeShadingNetworks", label="Write Shading Networks", default=False), BoolDef("writeTransformMatrix", label="Write Transform Matrix", default=True), BoolDef("writeUsdAttributes", label="Write USD Attributes", default=True), BoolDef("writeInstancesAsReferences", label="Write Instances as References", default=False), BoolDef("timeVaryingTopology", label="Time Varying Topology", default=False), TextDef("customMaterialNamespace", label="Custom Material Namespace", default=''), NumberDef("numTimeSamples", label="Num Time Samples", default=1), NumberDef("timeSamplesSpan", label="Time Samples Span", default=0.0), ]) return defs ================================================ FILE: openpype/hosts/maya/plugins/create/create_multiverse_usd_comp.py ================================================ from openpype.hosts.maya.api import plugin, lib from openpype.lib import ( BoolDef, NumberDef, EnumDef ) class CreateMultiverseUsdComp(plugin.MayaCreator): """Create Multiverse USD Composition""" identifier = "io.openpype.creators.maya.mvusdcomposition" label = "Multiverse USD Composition" family = "mvUsdComposition" icon = "cubes" def get_instance_attr_defs(self): defs = lib.collect_animation_defs(fps=True) defs.extend([ EnumDef("fileFormat", label="File format", items=["usd", "usda"], default="usd"), BoolDef("stripNamespaces", label="Strip Namespaces", default=False), BoolDef("mergeTransformAndShape", label="Merge Transform and Shape", default=False), BoolDef("flattenContent", label="Flatten Content", default=False), BoolDef("writeAsCompoundLayers", label="Write As Compound Layers", default=False), BoolDef("writePendingOverrides", label="Write Pending Overrides", default=False), NumberDef("numTimeSamples", label="Num Time Samples", default=1), NumberDef("timeSamplesSpan", label="Time Samples Span", default=0.0), ]) return defs ================================================ FILE: openpype/hosts/maya/plugins/create/create_multiverse_usd_over.py ================================================ from openpype.hosts.maya.api import plugin, lib from openpype.lib import ( BoolDef, NumberDef, EnumDef ) class CreateMultiverseUsdOver(plugin.MayaCreator): """Create Multiverse USD Override""" identifier = "io.openpype.creators.maya.mvusdoverride" label = "Multiverse USD Override" family = "mvUsdOverride" icon = "cubes" def get_instance_attr_defs(self): defs = lib.collect_animation_defs(fps=True) defs.extend([ EnumDef("fileFormat", label="File format", items=["usd", "usda"], default="usd"), BoolDef("writeAll", label="Write All", default=False), BoolDef("writeTransforms", label="Write Transforms", default=True), BoolDef("writeVisibility", label="Write Visibility", default=True), BoolDef("writeAttributes", label="Write Attributes", default=True), BoolDef("writeMaterials", label="Write Materials", default=True), BoolDef("writeVariants", label="Write Variants", default=True), BoolDef("writeVariantsDefinition", label="Write Variants Definition", default=True), BoolDef("writeActiveState", label="Write Active State", default=True), BoolDef("writeNamespaces", label="Write Namespaces", default=False), NumberDef("numTimeSamples", label="Num Time Samples", default=1), NumberDef("timeSamplesSpan", label="Time Samples Span", default=0.0), ]) return defs ================================================ FILE: openpype/hosts/maya/plugins/create/create_proxy_abc.py ================================================ from openpype.hosts.maya.api import ( lib, plugin ) from openpype.lib import ( BoolDef, TextDef ) class CreateProxyAlembic(plugin.MayaCreator): """Proxy Alembic for animated data""" identifier = "io.openpype.creators.maya.proxyabc" label = "Proxy Alembic" family = "proxyAbc" icon = "gears" write_color_sets = False write_face_sets = False def get_instance_attr_defs(self): defs = lib.collect_animation_defs() defs.extend([ BoolDef("writeColorSets", label="Write vertex colors", tooltip="Write vertex colors with the geometry", default=self.write_color_sets), BoolDef("writeFaceSets", label="Write face sets", tooltip="Write face sets with the geometry", default=self.write_face_sets), BoolDef("worldSpace", label="World-Space Export", default=True), TextDef("nameSuffix", label="Name Suffix for Bounding Box", default="_BBox", placeholder="_BBox"), TextDef("attr", label="Custom Attributes", default="", placeholder="attr1, attr2"), TextDef("attrPrefix", label="Custom Attributes Prefix", placeholder="prefix1, prefix2") ]) return defs ================================================ FILE: openpype/hosts/maya/plugins/create/create_redshift_proxy.py ================================================ # -*- coding: utf-8 -*- """Creator of Redshift proxy subset types.""" from openpype.hosts.maya.api import plugin, lib from openpype.lib import BoolDef class CreateRedshiftProxy(plugin.MayaCreator): """Create instance of Redshift Proxy subset.""" identifier = "io.openpype.creators.maya.redshiftproxy" label = "Redshift Proxy" family = "redshiftproxy" icon = "gears" def get_instance_attr_defs(self): defs = [ BoolDef("animation", label="Export animation", default=False) ] defs.extend(lib.collect_animation_defs()) return defs ================================================ FILE: openpype/hosts/maya/plugins/create/create_render.py ================================================ # -*- coding: utf-8 -*- """Create ``Render`` instance in Maya.""" from openpype.hosts.maya.api import ( lib_rendersettings, plugin ) from openpype.pipeline import CreatorError from openpype.lib import ( BoolDef, NumberDef, ) class CreateRenderlayer(plugin.RenderlayerCreator): """Create and manages renderlayer subset per renderLayer in workfile. This generates a single node in the scene which tells the Creator to if it exists collect Maya rendersetup renderlayers as individual instances. As such, triggering create doesn't actually create the instance node per layer but only the node which tells the Creator it may now collect the renderlayers. """ identifier = "io.openpype.creators.maya.renderlayer" family = "renderlayer" label = "Render" icon = "eye" layer_instance_prefix = "render" singleton_node_name = "renderingMain" render_settings = {} @classmethod def apply_settings(cls, project_settings): cls.render_settings = project_settings["maya"]["RenderSettings"] def create(self, subset_name, instance_data, pre_create_data): # Only allow a single render instance to exist if self._get_singleton_node(): raise CreatorError("A Render instance already exists - only " "one can be configured.") # Apply default project render settings on create if self.render_settings.get("apply_render_settings"): lib_rendersettings.RenderSettings().set_default_renderer_settings() super(CreateRenderlayer, self).create(subset_name, instance_data, pre_create_data) def get_instance_attr_defs(self): """Create instance settings.""" return [ BoolDef("review", label="Review", tooltip="Mark as reviewable", default=True), BoolDef("extendFrames", label="Extend Frames", tooltip="Extends the frames on top of the previous " "publish.\nIf the previous was 1001-1050 and you " "would now submit 1020-1070 only the new frames " "1051-1070 would be rendered and published " "together with the previously rendered frames.\n" "If 'overrideExistingFrame' is enabled it *will* " "render any existing frames.", default=False), BoolDef("overrideExistingFrame", label="Override Existing Frame", tooltip="Override existing rendered frames " "(if they exist).", default=True), # TODO: Should these move to submit_maya_deadline plugin? # Tile rendering BoolDef("tileRendering", label="Enable tiled rendering", default=False), NumberDef("tilesX", label="Tiles X", default=2, minimum=1, decimals=0), NumberDef("tilesY", label="Tiles Y", default=2, minimum=1, decimals=0), # Additional settings BoolDef("convertToScanline", label="Convert to Scanline", tooltip="Convert the output images to scanline images", default=False), BoolDef("useReferencedAovs", label="Use Referenced AOVs", tooltip="Consider the AOVs from referenced scenes as well", default=False), BoolDef("renderSetupIncludeLights", label="Render Setup Include Lights", default=self.render_settings.get("enable_all_lights", False)) ] ================================================ FILE: openpype/hosts/maya/plugins/create/create_rendersetup.py ================================================ from openpype.hosts.maya.api import plugin from openpype.pipeline import CreatorError class CreateRenderSetup(plugin.MayaCreator): """Create rendersetup template json data""" identifier = "io.openpype.creators.maya.rendersetup" label = "Render Setup Preset" family = "rendersetup" icon = "tablet" def get_pre_create_attr_defs(self): # Do not show the "use_selection" setting from parent class return [] def create(self, subset_name, instance_data, pre_create_data): existing_instance = None for instance in self.create_context.instances: if instance.family == self.family: existing_instance = instance break if existing_instance: raise CreatorError("A RenderSetup instance already exists - only " "one can be configured.") super(CreateRenderSetup, self).create(subset_name, instance_data, pre_create_data) ================================================ FILE: openpype/hosts/maya/plugins/create/create_review.py ================================================ import json from maya import cmds from openpype import AYON_SERVER_ENABLED from openpype.hosts.maya.api import ( lib, plugin ) from openpype.lib import ( BoolDef, NumberDef, EnumDef ) from openpype.pipeline import CreatedInstance from openpype.client import get_asset_by_name TRANSPARENCIES = [ "preset", "simple", "object sorting", "weighted average", "depth peeling", "alpha cut" ] class CreateReview(plugin.MayaCreator): """Playblast reviewable""" identifier = "io.openpype.creators.maya.review" label = "Review" family = "review" icon = "video-camera" useMayaTimeline = True panZoom = False # Overriding "create" method to prefill values from settings. def create(self, subset_name, instance_data, pre_create_data): members = list() if pre_create_data.get("use_selection"): members = cmds.ls(selection=True) project_name = self.project_name if AYON_SERVER_ENABLED: asset_name = instance_data["folderPath"] else: asset_name = instance_data["asset"] asset_doc = get_asset_by_name(project_name, asset_name) task_name = instance_data["task"] preset = lib.get_capture_preset( task_name, asset_doc["data"]["tasks"][task_name]["type"], subset_name, self.project_settings, self.log ) self.log.debug( "Using preset: {}".format( json.dumps(preset, indent=4, sort_keys=True) ) ) with lib.undo_chunk(): instance_node = cmds.sets(members, name=subset_name) instance_data["instance_node"] = instance_node instance = CreatedInstance( self.family, subset_name, instance_data, self) creator_attribute_defs_by_key = { x.key: x for x in instance.creator_attribute_defs } mapping = { "review_width": preset["Resolution"]["width"], "review_height": preset["Resolution"]["height"], "isolate": preset["Generic"]["isolate_view"], "imagePlane": preset["Viewport Options"]["imagePlane"], "panZoom": preset["Generic"]["pan_zoom"] } for key, value in mapping.items(): creator_attribute_defs_by_key[key].default = value self._add_instance_to_context(instance) self.imprint_instance_node(instance_node, data=instance.data_to_store()) return instance def get_instance_attr_defs(self): defs = lib.collect_animation_defs() # Option for using Maya or asset frame range in settings. if not self.useMayaTimeline: # Update the defaults to be the asset frame range frame_range = lib.get_frame_range() defs_by_key = {attr_def.key: attr_def for attr_def in defs} for key, value in frame_range.items(): if key not in defs_by_key: raise RuntimeError("Attribute definition not found to be " "updated for key: {}".format(key)) attr_def = defs_by_key[key] attr_def.default = value defs.extend([ NumberDef("review_width", label="Review width", tooltip="A value of zero will use the asset resolution.", decimals=0, minimum=0, default=0), NumberDef("review_height", label="Review height", tooltip="A value of zero will use the asset resolution.", decimals=0, minimum=0, default=0), BoolDef("keepImages", label="Keep Images", tooltip="Whether to also publish along the image sequence " "next to the video reviewable.", default=False), BoolDef("isolate", label="Isolate render members of instance", tooltip="When enabled only the members of the instance " "will be included in the playblast review.", default=False), BoolDef("imagePlane", label="Show Image Plane", default=True), EnumDef("transparency", label="Transparency", items=TRANSPARENCIES), BoolDef("panZoom", label="Enable camera pan/zoom", default=True), EnumDef("displayLights", label="Display Lights", items=lib.DISPLAY_LIGHTS_ENUM), ]) return defs ================================================ FILE: openpype/hosts/maya/plugins/create/create_rig.py ================================================ from maya import cmds from openpype.hosts.maya.api import plugin class CreateRig(plugin.MayaCreator): """Artist-friendly rig with controls to direct motion""" identifier = "io.openpype.creators.maya.rig" label = "Rig" family = "rig" icon = "wheelchair" def create(self, subset_name, instance_data, pre_create_data): instance = super(CreateRig, self).create(subset_name, instance_data, pre_create_data) instance_node = instance.get("instance_node") self.log.info("Creating Rig instance set up ...") # TODO:change name (_controls_SET -> _rigs_SET) controls = cmds.sets(name=subset_name + "_controls_SET", empty=True) # TODO:change name (_out_SET -> _geo_SET) pointcache = cmds.sets(name=subset_name + "_out_SET", empty=True) skeleton = cmds.sets( name=subset_name + "_skeletonAnim_SET", empty=True) skeleton_mesh = cmds.sets( name=subset_name + "_skeletonMesh_SET", empty=True) cmds.sets([controls, pointcache, skeleton, skeleton_mesh], forceElement=instance_node) ================================================ FILE: openpype/hosts/maya/plugins/create/create_setdress.py ================================================ from openpype.hosts.maya.api import plugin from openpype.lib import BoolDef class CreateSetDress(plugin.MayaCreator): """A grouped package of loaded content""" identifier = "io.openpype.creators.maya.setdress" label = "Set Dress" family = "setdress" icon = "cubes" default_variants = ["Main", "Anim"] def get_instance_attr_defs(self): return [ BoolDef("exactSetMembersOnly", label="Exact Set Members Only", default=True) ] ================================================ FILE: openpype/hosts/maya/plugins/create/create_unreal_skeletalmesh.py ================================================ # -*- coding: utf-8 -*- """Creator for Unreal Skeletal Meshes.""" from openpype.hosts.maya.api import plugin, lib from openpype.lib import ( BoolDef, TextDef ) from maya import cmds # noqa class CreateUnrealSkeletalMesh(plugin.MayaCreator): """Unreal Static Meshes with collisions.""" identifier = "io.openpype.creators.maya.unrealskeletalmesh" label = "Unreal - Skeletal Mesh" family = "skeletalMesh" icon = "thumbs-up" dynamic_subset_keys = ["asset"] # Defined in settings joint_hints = set() def apply_settings(self, project_settings): """Apply project settings to creator""" settings = ( project_settings["maya"]["create"]["CreateUnrealSkeletalMesh"] ) self.joint_hints = set(settings.get("joint_hints", [])) def get_dynamic_data( self, variant, task_name, asset_doc, project_name, host_name, instance ): """ The default subset name templates for Unreal include {asset} and thus we should pass that along as dynamic data. """ dynamic_data = super(CreateUnrealSkeletalMesh, self).get_dynamic_data( variant, task_name, asset_doc, project_name, host_name, instance ) dynamic_data["asset"] = asset_doc["name"] return dynamic_data def create(self, subset_name, instance_data, pre_create_data): with lib.undo_chunk(): instance = super(CreateUnrealSkeletalMesh, self).create( subset_name, instance_data, pre_create_data) instance_node = instance.get("instance_node") # We reorganize the geometry that was originally added into the # set into either 'joints_SET' or 'geometry_SET' based on the # joint_hints from project settings members = cmds.sets(instance_node, query=True) or [] cmds.sets(clear=instance_node) geometry_set = cmds.sets(name="geometry_SET", empty=True) joints_set = cmds.sets(name="joints_SET", empty=True) cmds.sets([geometry_set, joints_set], forceElement=instance_node) for node in members: if node in self.joint_hints: cmds.sets(node, forceElement=joints_set) else: cmds.sets(node, forceElement=geometry_set) def get_instance_attr_defs(self): defs = lib.collect_animation_defs() defs.extend([ BoolDef("renderableOnly", label="Renderable Only", tooltip="Only export renderable visible shapes", default=False), BoolDef("visibleOnly", label="Visible Only", tooltip="Only export dag objects visible during " "frame range", default=False), BoolDef("includeParentHierarchy", label="Include Parent Hierarchy", tooltip="Whether to include parent hierarchy of nodes in " "the publish instance", default=False), BoolDef("worldSpace", label="World-Space Export", default=True), BoolDef("refresh", label="Refresh viewport during export", default=False), TextDef("attr", label="Custom Attributes", default="", placeholder="attr1, attr2"), TextDef("attrPrefix", label="Custom Attributes Prefix", placeholder="prefix1, prefix2") ]) return defs ================================================ FILE: openpype/hosts/maya/plugins/create/create_unreal_staticmesh.py ================================================ # -*- coding: utf-8 -*- """Creator for Unreal Static Meshes.""" from openpype.hosts.maya.api import plugin, lib from maya import cmds # noqa class CreateUnrealStaticMesh(plugin.MayaCreator): """Unreal Static Meshes with collisions.""" identifier = "io.openpype.creators.maya.unrealstaticmesh" label = "Unreal - Static Mesh" family = "staticMesh" icon = "cube" dynamic_subset_keys = ["asset"] # Defined in settings collision_prefixes = [] def apply_settings(self, project_settings): """Apply project settings to creator""" settings = project_settings["maya"]["create"]["CreateUnrealStaticMesh"] self.collision_prefixes = settings["collision_prefixes"] def get_dynamic_data( self, variant, task_name, asset_doc, project_name, host_name, instance ): """ The default subset name templates for Unreal include {asset} and thus we should pass that along as dynamic data. """ dynamic_data = super(CreateUnrealStaticMesh, self).get_dynamic_data( variant, task_name, asset_doc, project_name, host_name, instance ) dynamic_data["asset"] = asset_doc["name"] return dynamic_data def create(self, subset_name, instance_data, pre_create_data): with lib.undo_chunk(): instance = super(CreateUnrealStaticMesh, self).create( subset_name, instance_data, pre_create_data) instance_node = instance.get("instance_node") # We reorganize the geometry that was originally added into the # set into either 'collision_SET' or 'geometry_SET' based on the # collision_prefixes from project settings members = cmds.sets(instance_node, query=True) cmds.sets(clear=instance_node) geometry_set = cmds.sets(name="geometry_SET", empty=True) collisions_set = cmds.sets(name="collisions_SET", empty=True) cmds.sets([geometry_set, collisions_set], forceElement=instance_node) members = cmds.ls(members, long=True) or [] children = cmds.listRelatives(members, allDescendents=True, fullPath=True) or [] transforms = cmds.ls(members + children, type="transform") for transform in transforms: if not cmds.listRelatives(transform, type="shape", noIntermediate=True): # Exclude all transforms that have no direct shapes continue if self.has_collision_prefix(transform): cmds.sets(transform, forceElement=collisions_set) else: cmds.sets(transform, forceElement=geometry_set) def has_collision_prefix(self, node_path): """Return whether node name of path matches collision prefix. If the node name matches the collision prefix we add it to the `collisions_SET` instead of the `geometry_SET`. Args: node_path (str): Maya node path. Returns: bool: Whether the node should be considered a collision mesh. """ node_name = node_path.rsplit("|", 1)[-1] for prefix in self.collision_prefixes: if node_name.startswith(prefix): return True return False ================================================ FILE: openpype/hosts/maya/plugins/create/create_unreal_yeticache.py ================================================ from openpype.hosts.maya.api import ( lib, plugin ) from openpype.lib import NumberDef class CreateYetiCache(plugin.MayaCreator): """Output for procedural plugin nodes of Yeti """ identifier = "io.openpype.creators.maya.unrealyeticache" label = "Unreal - Yeti Cache" family = "yeticacheUE" icon = "pagelines" def get_instance_attr_defs(self): defs = [ NumberDef("preroll", label="Preroll", minimum=0, default=0, decimals=0) ] # Add animation data without step and handles defs.extend(lib.collect_animation_defs()) remove = {"step", "handleStart", "handleEnd"} defs = [attr_def for attr_def in defs if attr_def.key not in remove] # Add samples after frame range defs.append( NumberDef("samples", label="Samples", default=3, decimals=0) ) return defs ================================================ FILE: openpype/hosts/maya/plugins/create/create_vrayproxy.py ================================================ from openpype.hosts.maya.api import ( plugin, lib ) from openpype.lib import BoolDef class CreateVrayProxy(plugin.MayaCreator): """Alembic pointcache for animated data""" identifier = "io.openpype.creators.maya.vrayproxy" label = "VRay Proxy" family = "vrayproxy" icon = "gears" vrmesh = True alembic = True def get_instance_attr_defs(self): defs = [ BoolDef("animation", label="Export Animation", default=False) ] # Add time range attributes but remove some attributes # which this instance actually doesn't use defs.extend(lib.collect_animation_defs()) remove = {"handleStart", "handleEnd", "step"} defs = [attr_def for attr_def in defs if attr_def.key not in remove] defs.extend([ BoolDef("vertexColors", label="Write vertex colors", tooltip="Write vertex colors with the geometry", default=False), BoolDef("vrmesh", label="Export VRayMesh", tooltip="Publish a .vrmesh (VRayMesh) file for " "this VRayProxy", default=self.vrmesh), BoolDef("alembic", label="Export Alembic", tooltip="Publish a .abc (Alembic) file for " "this VRayProxy", default=self.alembic), ]) return defs ================================================ FILE: openpype/hosts/maya/plugins/create/create_vrayscene.py ================================================ # -*- coding: utf-8 -*- """Create instance of vrayscene.""" from openpype.hosts.maya.api import ( lib_rendersettings, plugin ) from openpype.pipeline import CreatorError from openpype.lib import BoolDef class CreateVRayScene(plugin.RenderlayerCreator): """Create Vray Scene.""" identifier = "io.openpype.creators.maya.vrayscene" family = "vrayscene" label = "VRay Scene" icon = "cubes" render_settings = {} singleton_node_name = "vraysceneMain" @classmethod def apply_settings(cls, project_settings): cls.render_settings = project_settings["maya"]["RenderSettings"] def create(self, subset_name, instance_data, pre_create_data): # Only allow a single render instance to exist if self._get_singleton_node(): raise CreatorError("A Render instance already exists - only " "one can be configured.") super(CreateVRayScene, self).create(subset_name, instance_data, pre_create_data) # Apply default project render settings on create if self.render_settings.get("apply_render_settings"): lib_rendersettings.RenderSettings().set_default_renderer_settings() def get_instance_attr_defs(self): """Create instance settings.""" return [ BoolDef("vraySceneMultipleFiles", label="V-Ray Scene Multiple Files", default=False), BoolDef("exportOnFarm", label="Export on farm", default=False) ] ================================================ FILE: openpype/hosts/maya/plugins/create/create_workfile.py ================================================ # -*- coding: utf-8 -*- """Creator plugin for creating workfiles.""" from openpype import AYON_SERVER_ENABLED from openpype.pipeline import CreatedInstance, AutoCreator from openpype.client import get_asset_by_name, get_asset_name_identifier from openpype.hosts.maya.api import plugin from maya import cmds class CreateWorkfile(plugin.MayaCreatorBase, AutoCreator): """Workfile auto-creator.""" identifier = "io.openpype.creators.maya.workfile" label = "Workfile" family = "workfile" icon = "fa5.file" default_variant = "Main" def create(self): variant = self.default_variant current_instance = next( ( instance for instance in self.create_context.instances if instance.creator_identifier == self.identifier ), None) project_name = self.project_name asset_name = self.create_context.get_current_asset_name() task_name = self.create_context.get_current_task_name() host_name = self.create_context.host_name if current_instance is None: current_instance_asset = None elif AYON_SERVER_ENABLED: current_instance_asset = current_instance["folderPath"] else: current_instance_asset = current_instance["asset"] if current_instance is None: asset_doc = get_asset_by_name(project_name, asset_name) subset_name = self.get_subset_name( variant, task_name, asset_doc, project_name, host_name ) data = { "task": task_name, "variant": variant } if AYON_SERVER_ENABLED: data["folderPath"] = asset_name else: data["asset"] = asset_name data.update( self.get_dynamic_data( variant, task_name, asset_doc, project_name, host_name, current_instance) ) self.log.info("Auto-creating workfile instance...") current_instance = CreatedInstance( self.family, subset_name, data, self ) self._add_instance_to_context(current_instance) elif ( current_instance_asset != asset_name or current_instance["task"] != task_name ): # Update instance context if is not the same asset_doc = get_asset_by_name(project_name, asset_name) subset_name = self.get_subset_name( variant, task_name, asset_doc, project_name, host_name ) asset_name = get_asset_name_identifier(asset_doc) if AYON_SERVER_ENABLED: current_instance["folderPath"] = asset_name else: current_instance["asset"] = asset_name current_instance["task"] = task_name current_instance["subset"] = subset_name def collect_instances(self): self.cache_subsets(self.collection_shared_data) cached_subsets = self.collection_shared_data["maya_cached_subsets"] for node in cached_subsets.get(self.identifier, []): node_data = self.read_instance_node(node) created_instance = CreatedInstance.from_existing(node_data, self) self._add_instance_to_context(created_instance) def update_instances(self, update_list): for created_inst, _changes in update_list: data = created_inst.data_to_store() node = data.get("instance_node") if not node: node = self.create_node() created_inst["instance_node"] = node data = created_inst.data_to_store() self.imprint_instance_node(node, data) def create_node(self): node = cmds.sets(empty=True, name="workfileMain") cmds.setAttr(node + ".hiddenInOutliner", True) return node ================================================ FILE: openpype/hosts/maya/plugins/create/create_xgen.py ================================================ from openpype.hosts.maya.api import plugin class CreateXgen(plugin.MayaCreator): """Xgen""" identifier = "io.openpype.creators.maya.xgen" label = "Xgen" family = "xgen" icon = "pagelines" ================================================ FILE: openpype/hosts/maya/plugins/create/create_yeti_cache.py ================================================ from openpype.hosts.maya.api import ( lib, plugin ) from openpype.lib import NumberDef class CreateYetiCache(plugin.MayaCreator): """Output for procedural plugin nodes of Yeti """ identifier = "io.openpype.creators.maya.yeticache" label = "Yeti Cache" family = "yeticache" icon = "pagelines" def get_instance_attr_defs(self): defs = [ NumberDef("preroll", label="Preroll", minimum=0, default=0, decimals=0) ] # Add animation data without step and handles defs.extend(lib.collect_animation_defs()) remove = {"step", "handleStart", "handleEnd"} defs = [attr_def for attr_def in defs if attr_def.key not in remove] # Add samples after frame range defs.append( NumberDef("samples", label="Samples", default=3, decimals=0) ) return defs ================================================ FILE: openpype/hosts/maya/plugins/create/create_yeti_rig.py ================================================ from maya import cmds from openpype.hosts.maya.api import ( lib, plugin ) class CreateYetiRig(plugin.MayaCreator): """Output for procedural plugin nodes ( Yeti / XGen / etc)""" identifier = "io.openpype.creators.maya.yetirig" label = "Yeti Rig" family = "yetiRig" icon = "usb" def create(self, subset_name, instance_data, pre_create_data): with lib.undo_chunk(): instance = super(CreateYetiRig, self).create(subset_name, instance_data, pre_create_data) instance_node = instance.get("instance_node") self.log.info("Creating Rig instance set up ...") input_meshes = cmds.sets(name="input_SET", empty=True) cmds.sets(input_meshes, forceElement=instance_node) ================================================ FILE: openpype/hosts/maya/plugins/inventory/connect_geometry.py ================================================ from maya import cmds from openpype.pipeline import InventoryAction, get_representation_context from openpype.hosts.maya.api.lib import get_id class ConnectGeometry(InventoryAction): """Connect geometries within containers. Source container will connect to the target containers, by searching for matching geometry IDs (cbid). Source containers are of family; "animation" and "pointcache". The connection with be done with a live world space blendshape. """ label = "Connect Geometry" icon = "link" color = "white" def process(self, containers): # Validate selection is more than 1. message = ( "Only 1 container selected. 2+ containers needed for this action." ) if len(containers) == 1: self.display_warning(message) return # Categorize containers by family. containers_by_family = {} for container in containers: family = get_representation_context( container["representation"] )["subset"]["data"]["family"] try: containers_by_family[family].append(container) except KeyError: containers_by_family[family] = [container] # Validate to only 1 source container. source_containers = containers_by_family.get("animation", []) source_containers += containers_by_family.get("pointcache", []) source_container_namespaces = [ x["namespace"] for x in source_containers ] message = ( "{} animation containers selected:\n\n{}\n\nOnly select 1 of type " "\"animation\" or \"pointcache\".".format( len(source_containers), source_container_namespaces ) ) if len(source_containers) != 1: self.display_warning(message) return source_object = source_containers[0]["objectName"] # Collect matching geometry transforms based cbId attribute. target_containers = [] for family, containers in containers_by_family.items(): if family in ["animation", "pointcache"]: continue target_containers.extend(containers) source_data = self.get_container_data(source_object) matches = [] node_types = set() for target_container in target_containers: target_data = self.get_container_data( target_container["objectName"] ) node_types.update(target_data["node_types"]) for id, transform in target_data["ids"].items(): source_match = source_data["ids"].get(id) if source_match: matches.append([source_match, transform]) # Message user about what is about to happen. if not matches: self.display_warning("No matching geometries found.") return message = "Connecting geometries:\n\n" for match in matches: message += "{} > {}\n".format(match[0], match[1]) choice = self.display_warning(message, show_cancel=True) if choice is False: return # Setup live worldspace blendshape connection. for source, target in matches: blendshape = cmds.blendShape(source, target)[0] cmds.setAttr(blendshape + ".origin", 0) cmds.setAttr(blendshape + "." + target.split(":")[-1], 1) # Update Xgen if in any of the containers. if "xgmPalette" in node_types: cmds.xgmPreview() def get_container_data(self, container): """Collects data about the container nodes. Args: container (dict): Container instance. Returns: data (dict): "node_types": All node types in container nodes. "ids": If the node is a mesh, we collect its parent transform id. """ data = {"node_types": set(), "ids": {}} ref_node = cmds.sets(container, query=True, nodesOnly=True)[0] for node in cmds.referenceQuery(ref_node, nodes=True): node_type = cmds.nodeType(node) data["node_types"].add(node_type) # Only interested in mesh transforms for connecting geometry with # blendshape. if node_type != "mesh": continue transform = cmds.listRelatives(node, parent=True)[0] data["ids"][get_id(transform)] = transform return data def display_warning(self, message, show_cancel=False): """Show feedback to user. Returns: bool """ from qtpy import QtWidgets accept = QtWidgets.QMessageBox.Ok if show_cancel: buttons = accept | QtWidgets.QMessageBox.Cancel else: buttons = accept state = QtWidgets.QMessageBox.warning( None, "", message, buttons=buttons, defaultButton=accept ) return state == accept ================================================ FILE: openpype/hosts/maya/plugins/inventory/connect_xgen.py ================================================ from maya import cmds import xgenm from openpype.pipeline import ( InventoryAction, get_representation_context, get_representation_path ) class ConnectXgen(InventoryAction): """Connect Xgen with an animation or pointcache. """ label = "Connect Xgen" icon = "link" color = "white" def process(self, containers): # Validate selection is more than 1. message = ( "Only 1 container selected. 2+ containers needed for this action." ) if len(containers) == 1: self.display_warning(message) return # Categorize containers by family. containers_by_family = {} for container in containers: family = get_representation_context( container["representation"] )["subset"]["data"]["family"] try: containers_by_family[family].append(container) except KeyError: containers_by_family[family] = [container] # Validate to only 1 source container. source_containers = containers_by_family.get("animation", []) source_containers += containers_by_family.get("pointcache", []) source_container_namespaces = [ x["namespace"] for x in source_containers ] message = ( "{} animation containers selected:\n\n{}\n\nOnly select 1 of type " "\"animation\" or \"pointcache\".".format( len(source_containers), source_container_namespaces ) ) if len(source_containers) != 1: self.display_warning(message) return source_container = source_containers[0] source_object = source_container["objectName"] # Validate source representation is an alembic. source_path = get_representation_path( get_representation_context( source_container["representation"] )["representation"] ).replace("\\", "/") message = "Animation container \"{}\" is not an alembic:\n{}".format( source_container["namespace"], source_path ) if not source_path.endswith(".abc"): self.display_warning(message) return # Target containers. target_containers = [] for family, containers in containers_by_family.items(): if family in ["animation", "pointcache"]: continue target_containers.extend(containers) # Inform user of connections from source representation to target # descriptions. descriptions_data = [] connections_msg = "" for target_container in target_containers: reference_node = cmds.sets( target_container["objectName"], query=True )[0] palettes = cmds.ls( cmds.referenceQuery(reference_node, nodes=True), type="xgmPalette" ) for palette in palettes: for description in xgenm.descriptions(palette): descriptions_data.append([palette, description]) connections_msg += "\n{}/{}".format(palette, description) message = "Connecting \"{}\" to:\n".format( source_container["namespace"] ) message += connections_msg choice = self.display_warning(message, show_cancel=True) if choice is False: return # Recreate "xgenContainers" attribute to reset. compound_name = "xgenContainers" attr = "{}.{}".format(source_object, compound_name) if cmds.objExists(attr): cmds.deleteAttr(attr) cmds.addAttr( source_object, longName=compound_name, attributeType="compound", numberOfChildren=1, multi=True ) # Connect target containers. for target_container in target_containers: cmds.addAttr( source_object, longName="container", attributeType="message", parent=compound_name ) index = target_containers.index(target_container) cmds.connectAttr( target_container["objectName"] + ".message", source_object + ".{}[{}].container".format( compound_name, index ) ) # Setup cache on Xgen object = "SplinePrimitive" for palette, description in descriptions_data: xgenm.setAttr("useCache", "true", palette, description, object) xgenm.setAttr("liveMode", "false", palette, description, object) xgenm.setAttr( "cacheFileName", source_path, palette, description, object ) # Refresh UI and viewport. de = xgenm.xgGlobal.DescriptionEditor de.refresh("Full") def display_warning(self, message, show_cancel=False): """Show feedback to user. Returns: bool """ from qtpy import QtWidgets accept = QtWidgets.QMessageBox.Ok if show_cancel: buttons = accept | QtWidgets.QMessageBox.Cancel else: buttons = accept state = QtWidgets.QMessageBox.warning( None, "", message, buttons=buttons, defaultButton=accept ) return state == accept ================================================ FILE: openpype/hosts/maya/plugins/inventory/connect_yeti_rig.py ================================================ import os import json from collections import defaultdict from maya import cmds from openpype.pipeline import ( InventoryAction, get_representation_context, get_representation_path ) from openpype.hosts.maya.api.lib import get_container_members, get_id class ConnectYetiRig(InventoryAction): """Connect Yeti Rig with an animation or pointcache.""" label = "Connect Yeti Rig" icon = "link" color = "white" def process(self, containers): # Validate selection is more than 1. message = ( "Only 1 container selected. 2+ containers needed for this action." ) if len(containers) == 1: self.display_warning(message) return # Categorize containers by family. containers_by_family = defaultdict(list) for container in containers: family = get_representation_context( container["representation"] )["subset"]["data"]["family"] containers_by_family[family].append(container) # Validate to only 1 source container. source_containers = containers_by_family.get("animation", []) source_containers += containers_by_family.get("pointcache", []) source_container_namespaces = [ x["namespace"] for x in source_containers ] message = ( "{} animation containers selected:\n\n{}\n\nOnly select 1 of type " "\"animation\" or \"pointcache\".".format( len(source_containers), source_container_namespaces ) ) if len(source_containers) != 1: self.display_warning(message) return source_container = source_containers[0] source_ids = self.nodes_by_id(source_container) # Target containers. target_ids = {} inputs = [] yeti_rig_containers = containers_by_family.get("yetiRig") if not yeti_rig_containers: self.display_warning( "Select at least one yetiRig container" ) return for container in yeti_rig_containers: target_ids.update(self.nodes_by_id(container)) maya_file = get_representation_path( get_representation_context( container["representation"] )["representation"] ) _, ext = os.path.splitext(maya_file) settings_file = maya_file.replace(ext, ".rigsettings") if not os.path.exists(settings_file): continue with open(settings_file) as f: inputs.extend(json.load(f)["inputs"]) # Compare loaded connections to scene. for input in inputs: source_node = source_ids.get(input["sourceID"]) target_node = target_ids.get(input["destinationID"]) if not source_node or not target_node: self.log.debug( "Could not find nodes for input:\n" + json.dumps(input, indent=4, sort_keys=True) ) continue source_attr, target_attr = input["connections"] if not cmds.attributeQuery( source_attr, node=source_node, exists=True ): self.log.debug( "Could not find attribute {} on node {} for " "input:\n{}".format( source_attr, source_node, json.dumps(input, indent=4, sort_keys=True) ) ) continue if not cmds.attributeQuery( target_attr, node=target_node, exists=True ): self.log.debug( "Could not find attribute {} on node {} for " "input:\n{}".format( target_attr, target_node, json.dumps(input, indent=4, sort_keys=True) ) ) continue source_plug = "{}.{}".format( source_node, source_attr ) target_plug = "{}.{}".format( target_node, target_attr ) if cmds.isConnected( source_plug, target_plug, ignoreUnitConversion=True ): self.log.debug( "Connection already exists: {} -> {}".format( source_plug, target_plug ) ) continue cmds.connectAttr(source_plug, target_plug, force=True) self.log.debug( "Connected attributes: {} -> {}".format( source_plug, target_plug ) ) def nodes_by_id(self, container): ids = {} for member in get_container_members(container): id = get_id(member) if not id: continue ids[id] = member return ids def display_warning(self, message, show_cancel=False): """Show feedback to user. Returns: bool """ from qtpy import QtWidgets accept = QtWidgets.QMessageBox.Ok if show_cancel: buttons = accept | QtWidgets.QMessageBox.Cancel else: buttons = accept state = QtWidgets.QMessageBox.warning( None, "", message, buttons=buttons, defaultButton=accept ) return state == accept ================================================ FILE: openpype/hosts/maya/plugins/inventory/import_modelrender.py ================================================ import re import json from openpype.client import ( get_representation_by_id, get_representations ) from openpype.pipeline import ( InventoryAction, get_representation_context, get_current_project_name, ) from openpype.hosts.maya.api.lib import ( maintained_selection, apply_shaders ) class ImportModelRender(InventoryAction): label = "Import Model Render Sets" icon = "industry" color = "#55DDAA" scene_type_regex = "meta.render.m[ab]" look_data_type = "meta.render.json" @staticmethod def is_compatible(container): return ( container.get("loader") == "ReferenceLoader" and container.get("name", "").startswith("model") ) def process(self, containers): from maya import cmds # noqa: F401 project_name = get_current_project_name() for container in containers: con_name = container["objectName"] nodes = [] for n in cmds.sets(con_name, query=True, nodesOnly=True) or []: if cmds.nodeType(n) == "reference": nodes += cmds.referenceQuery(n, nodes=True) else: nodes.append(n) repr_doc = get_representation_by_id( project_name, container["representation"], fields=["parent"] ) version_id = repr_doc["parent"] print("Importing render sets for model %r" % con_name) self.assign_model_render_by_version(nodes, version_id) def assign_model_render_by_version(self, nodes, version_id): """Assign nodes a specific published model render data version by id. This assumes the nodes correspond with the asset. Args: nodes(list): nodes to assign render data to version_id (bson.ObjectId): database id of the version of model Returns: None """ from maya import cmds # noqa: F401 project_name = get_current_project_name() repre_docs = get_representations( project_name, version_ids=[version_id], fields=["_id", "name"] ) # Get representations of shader file and relationships json_repre = None look_repres = [] scene_type_regex = re.compile(self.scene_type_regex) for repre_doc in repre_docs: repre_name = repre_doc["name"] if repre_name == self.look_data_type: json_repre = repre_doc continue if scene_type_regex.fullmatch(repre_name): look_repres.append(repre_doc) look_repre = look_repres[0] if look_repres else None # QUESTION shouldn't be json representation validated too? if not look_repre: print("No model render sets for this model version..") return context = get_representation_context(look_repre["_id"]) maya_file = self.filepath_from_context(context) context = get_representation_context(json_repre["_id"]) json_file = self.filepath_from_context(context) # Import the look file with maintained_selection(): shader_nodes = cmds.file(maya_file, i=True, # import returnNewNodes=True) # imprint context data # Load relationships shader_relation = json_file with open(shader_relation, "r") as f: relationships = json.load(f) # Assign relationships apply_shaders(relationships, shader_nodes, nodes) ================================================ FILE: openpype/hosts/maya/plugins/inventory/import_reference.py ================================================ from maya import cmds from openpype.pipeline import InventoryAction from openpype.hosts.maya.api.lib import get_reference_node class ImportReference(InventoryAction): """Imports selected reference to inside of the file.""" label = "Import Reference" icon = "download" color = "#d8d8d8" def process(self, containers): for container in containers: if container["loader"] != "ReferenceLoader": print("Not a reference, skipping") continue node = container["objectName"] members = cmds.sets(node, query=True, nodesOnly=True) ref_node = get_reference_node(members) ref_file = cmds.referenceQuery(ref_node, f=True) cmds.file(ref_file, importReference=True) return True # return anything to trigger model refresh ================================================ FILE: openpype/hosts/maya/plugins/inventory/rig_recreate_animation_instance.py ================================================ from openpype.pipeline import ( InventoryAction, get_representation_context ) from openpype.hosts.maya.api.lib import ( create_rig_animation_instance, get_container_members, ) class RecreateRigAnimationInstance(InventoryAction): """Recreate animation publish instance for loaded rigs""" label = "Recreate rig animation instance" icon = "wrench" color = "#888888" @staticmethod def is_compatible(container): return ( container.get("loader") == "ReferenceLoader" and container.get("name", "").startswith("rig") ) def process(self, containers): for container in containers: # todo: delete an existing entry if it exist or skip creation namespace = container["namespace"] representation_id = container["representation"] context = get_representation_context(representation_id) nodes = get_container_members(container) create_rig_animation_instance(nodes, context, namespace) ================================================ FILE: openpype/hosts/maya/plugins/inventory/select_containers.py ================================================ from maya import cmds from openpype.pipeline import InventoryAction, registered_host from openpype.hosts.maya.api.lib import get_container_members class SelectInScene(InventoryAction): """Select nodes in the scene from selected containers in scene inventory""" label = "Select in scene" icon = "search" color = "#888888" order = 99 def process(self, containers): all_members = [] for container in containers: members = get_container_members(container) all_members.extend(members) cmds.select(all_members, replace=True, noExpand=True) class HighlightBySceneSelection(InventoryAction): """Select containers in scene inventory from the current scene selection""" label = "Highlight by scene selection" icon = "search" color = "#888888" order = 100 def process(self, containers): selection = set(cmds.ls(selection=True, long=True, objectsOnly=True)) host = registered_host() to_select = [] for container in host.get_containers(): members = get_container_members(container) if any(member in selection for member in members): to_select.append(container["objectName"]) return { "objectNames": to_select, "options": {"clear": True} } ================================================ FILE: openpype/hosts/maya/plugins/load/_load_animation.py ================================================ import openpype.hosts.maya.api.plugin import maya.cmds as cmds def _process_reference(file_url, name, namespace, options): """Load files by referencing scene in Maya. Args: file_url (str): fileapth of the objects to be loaded name (str): subset name namespace (str): namespace options (dict): dict of storing the param Returns: list: list of object nodes """ from openpype.hosts.maya.api.lib import unique_namespace # Get name from asset being loaded # Assuming name is subset name from the animation, we split the number # suffix from the name to ensure the namespace is unique name = name.split("_")[0] ext = file_url.split(".")[-1] namespace = unique_namespace( "{}_".format(name), format="%03d", suffix="_{}".format(ext) ) attach_to_root = options.get("attach_to_root", True) group_name = options["group_name"] # no group shall be created if not attach_to_root: group_name = namespace nodes = cmds.file(file_url, namespace=namespace, sharedReferenceFile=False, groupReference=attach_to_root, groupName=group_name, reference=True, returnNewNodes=True) return nodes class AbcLoader(openpype.hosts.maya.api.plugin.ReferenceLoader): """Loader to reference an Alembic file""" families = ["animation", "camera", "pointcache"] representations = ["abc"] label = "Reference animation" order = -10 icon = "code-fork" color = "orange" def process_reference(self, context, name, namespace, options): cmds.loadPlugin("AbcImport.mll", quiet=True) # hero_001 (abc) # asset_counter{optional} path = self.filepath_from_context(context) file_url = self.prepare_root_value(path, context["project"]["name"]) nodes = _process_reference(file_url, name, namespace, options) # load colorbleed ID attribute self[:] = nodes return nodes class FbxLoader(openpype.hosts.maya.api.plugin.ReferenceLoader): """Loader to reference an Fbx files""" families = ["animation", "camera"] representations = ["fbx"] label = "Reference animation" order = -10 icon = "code-fork" color = "orange" def process_reference(self, context, name, namespace, options): cmds.loadPlugin("fbx4maya.mll", quiet=True) path = self.filepath_from_context(context) file_url = self.prepare_root_value(path, context["project"]["name"]) nodes = _process_reference(file_url, name, namespace, options) self[:] = nodes return nodes ================================================ FILE: openpype/hosts/maya/plugins/load/actions.py ================================================ """A module containing generic loader actions that will display in the Loader. """ import qargparse from openpype.pipeline import load from openpype.hosts.maya.api.lib import ( maintained_selection, get_custom_namespace ) import openpype.hosts.maya.api.plugin class SetFrameRangeLoader(load.LoaderPlugin): """Set frame range excluding pre- and post-handles""" families = ["animation", "camera", "proxyAbc", "pointcache"] representations = ["abc"] label = "Set frame range" order = 11 icon = "clock-o" color = "white" def load(self, context, name, namespace, data): import maya.cmds as cmds version = context['version'] version_data = version.get("data", {}) start = version_data.get("frameStart", None) end = version_data.get("frameEnd", None) if start is None or end is None: print("Skipping setting frame range because start or " "end frame data is missing..") return cmds.playbackOptions(minTime=start, maxTime=end, animationStartTime=start, animationEndTime=end) class SetFrameRangeWithHandlesLoader(load.LoaderPlugin): """Set frame range including pre- and post-handles""" families = ["animation", "camera", "proxyAbc", "pointcache"] representations = ["abc"] label = "Set frame range (with handles)" order = 12 icon = "clock-o" color = "white" def load(self, context, name, namespace, data): import maya.cmds as cmds version = context['version'] version_data = version.get("data", {}) start = version_data.get("frameStart", None) end = version_data.get("frameEnd", None) if start is None or end is None: print("Skipping setting frame range because start or " "end frame data is missing..") return # Include handles start -= version_data.get("handleStart", 0) end += version_data.get("handleEnd", 0) cmds.playbackOptions(minTime=start, maxTime=end, animationStartTime=start, animationEndTime=end) class ImportMayaLoader(openpype.hosts.maya.api.plugin.Loader): """Import action for Maya (unmanaged) Warning: The loaded content will be unmanaged and is *not* visible in the scene inventory. It's purely intended to merge content into your scene so you could also use it as a new base. """ representations = ["ma", "mb", "obj"] families = [ "model", "pointcache", "proxyAbc", "animation", "mayaAscii", "mayaScene", "setdress", "layout", "camera", "rig", "camerarig", "staticMesh", "workfile" ] label = "Import" order = 10 icon = "arrow-circle-down" color = "#775555" options = [ qargparse.Boolean( "clean_import", label="Clean import", default=False, help="Should all occurrences of cbId be purged?" ) ] def load(self, context, name=None, namespace=None, data=None): import maya.cmds as cmds choice = self.display_warning() if choice is False: return custom_group_name, custom_namespace, options = \ self.get_custom_namespace_and_group(context, data, "import_loader") namespace = get_custom_namespace(custom_namespace) if not options.get("attach_to_root", True): custom_group_name = namespace path = self.filepath_from_context(context) with maintained_selection(): nodes = cmds.file(path, i=True, preserveReferences=True, namespace=namespace, returnNewNodes=True, groupReference=options.get("attach_to_root", True), groupName=custom_group_name) if data.get("clean_import", False): remove_attributes = ["cbId"] for node in nodes: for attr in remove_attributes: if cmds.attributeQuery(attr, node=node, exists=True): full_attr = "{}.{}".format(node, attr) print("Removing {}".format(full_attr)) cmds.deleteAttr(full_attr) # We do not containerize imported content, it remains unmanaged return def display_warning(self): """Show warning to ensure the user can't import models by accident Returns: bool """ from qtpy import QtWidgets accept = QtWidgets.QMessageBox.Ok buttons = accept | QtWidgets.QMessageBox.Cancel message = "Are you sure you want import this" state = QtWidgets.QMessageBox.warning(None, "Are you sure?", message, buttons=buttons, defaultButton=accept) return state == accept ================================================ FILE: openpype/hosts/maya/plugins/load/load_arnold_standin.py ================================================ import os import clique import maya.cmds as cmds from openpype.settings import get_project_settings from openpype.pipeline import ( load, legacy_io, get_representation_path ) from openpype.hosts.maya.api.lib import ( unique_namespace, get_attribute_input, maintained_selection, convert_to_maya_fps ) from openpype.hosts.maya.api.pipeline import containerise def is_sequence(files): sequence = False collections, remainder = clique.assemble(files, minimum_items=1) if collections: sequence = True return sequence def get_current_session_fps(): session_fps = float(legacy_io.Session.get('AVALON_FPS', 25)) return convert_to_maya_fps(session_fps) class ArnoldStandinLoader(load.LoaderPlugin): """Load as Arnold standin""" families = [ "ass", "assProxy", "animation", "model", "proxyAbc", "pointcache", "usd" ] representations = ["ass", "abc", "usda", "usdc", "usd"] label = "Load as Arnold standin" order = -5 icon = "code-fork" color = "orange" def load(self, context, name, namespace, options): if not cmds.pluginInfo("mtoa", query=True, loaded=True): cmds.loadPlugin("mtoa") # Create defaultArnoldRenderOptions before creating aiStandin # which tries to connect it. Since we load the plugin and directly # create aiStandin without the defaultArnoldRenderOptions, # we need to create the render options for aiStandin creation. from mtoa.core import createOptions createOptions() import mtoa.ui.arnoldmenu version = context['version'] version_data = version.get("data", {}) self.log.info("version_data: {}\n".format(version_data)) asset = context['asset']['name'] namespace = namespace or unique_namespace( asset + "_", prefix="_" if asset[0].isdigit() else "", suffix="_", ) # Root group label = "{}:{}".format(namespace, name) root = cmds.group(name=label, empty=True) # Set color. settings = get_project_settings(context["project"]["name"]) color = settings['maya']['load']['colors'].get('ass') if color is not None: cmds.setAttr(root + ".useOutlinerColor", True) cmds.setAttr( root + ".outlinerColor", color[0], color[1], color[2] ) with maintained_selection(): # Create transform with shape transform_name = label + "_standin" standin_shape = mtoa.ui.arnoldmenu.createStandIn() standin = cmds.listRelatives(standin_shape, parent=True)[0] standin = cmds.rename(standin, transform_name) standin_shape = cmds.listRelatives(standin, shapes=True)[0] cmds.parent(standin, root) # Set the standin filepath repre_path = self.filepath_from_context(context) path, operator = self._setup_proxy( standin_shape, repre_path, namespace ) cmds.setAttr(standin_shape + ".dso", path, type="string") sequence = is_sequence(os.listdir(os.path.dirname(repre_path))) cmds.setAttr(standin_shape + ".useFrameExtension", sequence) fps = version["data"].get("fps") or get_current_session_fps() cmds.setAttr(standin_shape + ".abcFPS", float(fps)) nodes = [root, standin, standin_shape] if operator is not None: nodes.append(operator) self[:] = nodes return containerise( name=name, namespace=namespace, nodes=nodes, context=context, loader=self.__class__.__name__) def get_next_free_multi_index(self, attr_name): """Find the next unconnected multi index at the input attribute.""" for index in range(10000000): connection_info = cmds.connectionInfo( "{}[{}]".format(attr_name, index), sourceFromDestination=True ) if len(connection_info or []) == 0: return index def _get_proxy_path(self, path): basename_split = os.path.basename(path).split(".") proxy_basename = ( basename_split[0] + "_proxy." + ".".join(basename_split[1:]) ) proxy_path = "/".join([os.path.dirname(path), proxy_basename]) return proxy_basename, proxy_path def _update_operators(self, string_replace_operator, proxy_basename, path): cmds.setAttr( string_replace_operator + ".match", proxy_basename.split(".")[0], type="string" ) cmds.setAttr( string_replace_operator + ".replace", os.path.basename(path).split(".")[0], type="string" ) def _setup_proxy(self, shape, path, namespace): proxy_basename, proxy_path = self._get_proxy_path(path) options_node = "defaultArnoldRenderOptions" merge_operator = get_attribute_input(options_node + ".operator") if merge_operator is None: merge_operator = cmds.createNode("aiMerge") cmds.connectAttr( merge_operator + ".message", options_node + ".operator" ) merge_operator = merge_operator.split(".")[0] string_replace_operator = cmds.createNode( "aiStringReplace", name=namespace + ":string_replace_operator" ) node_type = "alembic" if path.endswith(".abc") else "procedural" cmds.setAttr( string_replace_operator + ".selection", "*.(@node=='{}')".format(node_type), type="string" ) self._update_operators(string_replace_operator, proxy_basename, path) cmds.connectAttr( string_replace_operator + ".out", "{}.inputs[{}]".format( merge_operator, self.get_next_free_multi_index(merge_operator + ".inputs") ) ) # We setup the string operator no matter whether there is a proxy or # not. This makes it easier to update since the string operator will # always be created. Return original path to use for standin. if not os.path.exists(proxy_path): return path, string_replace_operator return proxy_path, string_replace_operator def update(self, container, representation): # Update the standin members = cmds.sets(container['objectName'], query=True) for member in members: if cmds.nodeType(member) == "aiStringReplace": string_replace_operator = member shapes = cmds.listRelatives(member, shapes=True) if not shapes: continue if cmds.nodeType(shapes[0]) == "aiStandIn": standin = shapes[0] path = get_representation_path(representation) proxy_basename, proxy_path = self._get_proxy_path(path) # Whether there is proxy or not, we still update the string operator. # If no proxy exists, the string operator won't replace anything. self._update_operators(string_replace_operator, proxy_basename, path) dso_path = path if os.path.exists(proxy_path): dso_path = proxy_path cmds.setAttr(standin + ".dso", dso_path, type="string") sequence = is_sequence(os.listdir(os.path.dirname(path))) cmds.setAttr(standin + ".useFrameExtension", sequence) cmds.setAttr( container["objectName"] + ".representation", str(representation["_id"]), type="string" ) def switch(self, container, representation): self.update(container, representation) def remove(self, container): members = cmds.sets(container['objectName'], query=True) cmds.lockNode(members, lock=False) cmds.delete([container['objectName']] + members) # Clean up the namespace try: cmds.namespace(removeNamespace=container['namespace'], deleteNamespaceContent=True) except RuntimeError: pass ================================================ FILE: openpype/hosts/maya/plugins/load/load_assembly.py ================================================ import maya.cmds as cmds from openpype.pipeline import ( load, remove_container ) from openpype.hosts.maya.api.pipeline import containerise from openpype.hosts.maya.api.lib import unique_namespace from openpype.hosts.maya.api import setdress class AssemblyLoader(load.LoaderPlugin): families = ["assembly"] representations = ["json"] label = "Load Set Dress" order = -9 icon = "code-fork" color = "orange" def load(self, context, name, namespace, data): asset = context['asset']['name'] namespace = namespace or unique_namespace( asset + "_", prefix="_" if asset[0].isdigit() else "", suffix="_", ) containers = setdress.load_package( filepath=self.filepath_from_context(context), name=name, namespace=namespace ) self[:] = containers # Only containerize if any nodes were loaded by the Loader nodes = self[:] if not nodes: return return containerise( name=name, namespace=namespace, nodes=nodes, context=context, loader=self.__class__.__name__) def update(self, container, representation): return setdress.update_package(container, representation) def remove(self, container): """Remove all sub containers""" # Remove all members member_containers = setdress.get_contained_containers(container) for member_container in member_containers: self.log.info("Removing container %s", member_container['objectName']) remove_container(member_container) # Remove alembic hierarchy reference # TODO: Check whether removing all contained references is safe enough members = cmds.sets(container['objectName'], query=True) or [] references = cmds.ls(members, type="reference") for reference in references: self.log.info("Removing %s", reference) fname = cmds.referenceQuery(reference, filename=True) cmds.file(fname, removeReference=True) # Delete container and its contents if cmds.objExists(container['objectName']): members = cmds.sets(container['objectName'], query=True) or [] cmds.delete([container['objectName']] + members) # TODO: Ensure namespace is gone ================================================ FILE: openpype/hosts/maya/plugins/load/load_audio.py ================================================ from maya import cmds, mel from openpype.pipeline import ( load, get_representation_path, ) from openpype.hosts.maya.api.pipeline import containerise from openpype.hosts.maya.api.lib import unique_namespace, get_container_members class AudioLoader(load.LoaderPlugin): """Specific loader of audio.""" families = ["audio"] label = "Load audio" representations = ["wav"] icon = "volume-up" color = "orange" def load(self, context, name, namespace, data): start_frame = cmds.playbackOptions(query=True, min=True) sound_node = cmds.sound( file=self.filepath_from_context(context), offset=start_frame ) cmds.timeControl( mel.eval("$gPlayBackSlider=$gPlayBackSlider"), edit=True, sound=sound_node, displaySound=True ) asset = context["asset"]["name"] namespace = namespace or unique_namespace( asset + "_", prefix="_" if asset[0].isdigit() else "", suffix="_", ) return containerise( name=name, namespace=namespace, nodes=[sound_node], context=context, loader=self.__class__.__name__ ) def update(self, container, representation): members = get_container_members(container) audio_nodes = cmds.ls(members, type="audio") assert audio_nodes is not None, "Audio node not found." audio_node = audio_nodes[0] current_sound = cmds.timeControl( mel.eval("$gPlayBackSlider=$gPlayBackSlider"), query=True, sound=True ) activate_sound = current_sound == audio_node path = get_representation_path(representation) cmds.sound( audio_node, edit=True, file=path ) # The source start + end does not automatically update itself to the # length of thew new audio file, even though maya does do that when # creating a new audio node. So to update we compute it manually. # This would however override any source start and source end a user # might have done on the original audio node after load. audio_frame_count = cmds.getAttr("{}.frameCount".format(audio_node)) audio_sample_rate = cmds.getAttr("{}.sampleRate".format(audio_node)) duration_in_seconds = audio_frame_count / audio_sample_rate fps = mel.eval('currentTimeUnitToFPS()') # workfile FPS source_start = 0 source_end = (duration_in_seconds * fps) cmds.setAttr("{}.sourceStart".format(audio_node), source_start) cmds.setAttr("{}.sourceEnd".format(audio_node), source_end) if activate_sound: # maya by default deactivates it from timeline on file change cmds.timeControl( mel.eval("$gPlayBackSlider=$gPlayBackSlider"), edit=True, sound=audio_node, displaySound=True ) cmds.setAttr( container["objectName"] + ".representation", str(representation["_id"]), type="string" ) def switch(self, container, representation): self.update(container, representation) def remove(self, container): members = cmds.sets(container['objectName'], query=True) cmds.lockNode(members, lock=False) cmds.delete([container['objectName']] + members) # Clean up the namespace try: cmds.namespace(removeNamespace=container['namespace'], deleteNamespaceContent=True) except RuntimeError: pass ================================================ FILE: openpype/hosts/maya/plugins/load/load_gpucache.py ================================================ import os import maya.cmds as cmds from openpype.hosts.maya.api.pipeline import containerise from openpype.hosts.maya.api.lib import unique_namespace from openpype.pipeline import ( load, get_representation_path ) from openpype.settings import get_project_settings class GpuCacheLoader(load.LoaderPlugin): """Load Alembic as gpuCache""" families = ["model", "animation", "proxyAbc", "pointcache"] representations = ["abc", "gpu_cache"] label = "Load Gpu Cache" order = -5 icon = "code-fork" color = "orange" def load(self, context, name, namespace, data): asset = context['asset']['name'] namespace = namespace or unique_namespace( asset + "_", prefix="_" if asset[0].isdigit() else "", suffix="_", ) cmds.loadPlugin("gpuCache", quiet=True) # Root group label = "{}:{}".format(namespace, name) root = cmds.group(name=label, empty=True) project_name = context["project"]["name"] settings = get_project_settings(project_name) colors = settings['maya']['load']['colors'] c = colors.get('model') if c is not None: cmds.setAttr(root + ".useOutlinerColor", 1) cmds.setAttr( root + ".outlinerColor", (float(c[0]) / 255), (float(c[1]) / 255), (float(c[2]) / 255) ) # Create transform with shape transform_name = label + "_GPU" transform = cmds.createNode("transform", name=transform_name, parent=root) cache = cmds.createNode("gpuCache", parent=transform, name="{0}Shape".format(transform_name)) # Set the cache filepath path = self.filepath_from_context(context) cmds.setAttr(cache + '.cacheFileName', path, type="string") cmds.setAttr(cache + '.cacheGeomPath', "|", type="string") # root # Lock parenting of the transform and cache cmds.lockNode([transform, cache], lock=True) nodes = [root, transform, cache] self[:] = nodes return containerise( name=name, namespace=namespace, nodes=nodes, context=context, loader=self.__class__.__name__) def update(self, container, representation): path = get_representation_path(representation) # Update the cache members = cmds.sets(container['objectName'], query=True) caches = cmds.ls(members, type="gpuCache", long=True) assert len(caches) == 1, "This is a bug" for cache in caches: cmds.setAttr(cache + ".cacheFileName", path, type="string") cmds.setAttr(container["objectName"] + ".representation", str(representation["_id"]), type="string") def switch(self, container, representation): self.update(container, representation) def remove(self, container): members = cmds.sets(container['objectName'], query=True) cmds.lockNode(members, lock=False) cmds.delete([container['objectName']] + members) # Clean up the namespace try: cmds.namespace(removeNamespace=container['namespace'], deleteNamespaceContent=True) except RuntimeError: pass ================================================ FILE: openpype/hosts/maya/plugins/load/load_image.py ================================================ import os import copy from openpype.lib import EnumDef from openpype.pipeline import ( load, get_representation_context, get_current_host_name, ) from openpype.pipeline.load.utils import get_representation_path_from_context from openpype.pipeline.colorspace import ( get_imageio_file_rules_colorspace_from_filepath, get_imageio_config, get_imageio_file_rules ) from openpype.settings import get_project_settings from openpype.hosts.maya.api.pipeline import containerise from openpype.hosts.maya.api.lib import ( unique_namespace, namespaced ) from maya import cmds def create_texture(): """Create place2dTexture with file node with uv connections Mimics Maya "file [Texture]" creation. """ place = cmds.shadingNode("place2dTexture", asUtility=True, name="place2d") file = cmds.shadingNode("file", asTexture=True, name="file") connections = ["coverage", "translateFrame", "rotateFrame", "rotateUV", "mirrorU", "mirrorV", "stagger", "wrapV", "wrapU", "repeatUV", "offset", "noiseUV", "vertexUvThree", "vertexUvTwo", "vertexUvOne", "vertexCameraOne"] for attr in connections: src = "{}.{}".format(place, attr) dest = "{}.{}".format(file, attr) cmds.connectAttr(src, dest) cmds.connectAttr(place + '.outUV', file + '.uvCoord') cmds.connectAttr(place + '.outUvFilterSize', file + '.uvFilterSize') return file, place def create_projection(): """Create texture with place3dTexture and projection Mimics Maya "file [Projection]" creation. """ file, place = create_texture() projection = cmds.shadingNode("projection", asTexture=True, name="projection") place3d = cmds.shadingNode("place3dTexture", asUtility=True, name="place3d") cmds.connectAttr(place3d + '.worldInverseMatrix[0]', projection + ".placementMatrix") cmds.connectAttr(file + '.outColor', projection + ".image") return file, place, projection, place3d def create_stencil(): """Create texture with extra place2dTexture offset and stencil Mimics Maya "file [Stencil]" creation. """ file, place = create_texture() place_stencil = cmds.shadingNode("place2dTexture", asUtility=True, name="place2d_stencil") stencil = cmds.shadingNode("stencil", asTexture=True, name="stencil") for src_attr, dest_attr in [ ("outUV", "uvCoord"), ("outUvFilterSize", "uvFilterSize") ]: src_plug = "{}.{}".format(place_stencil, src_attr) cmds.connectAttr(src_plug, "{}.{}".format(place, dest_attr)) cmds.connectAttr(src_plug, "{}.{}".format(stencil, dest_attr)) return file, place, stencil, place_stencil class FileNodeLoader(load.LoaderPlugin): """File node loader.""" families = ["image", "plate", "render"] label = "Load file node" representations = ["exr", "tif", "png", "jpg"] icon = "image" color = "orange" order = 2 options = [ EnumDef( "mode", items={ "texture": "Texture", "projection": "Projection", "stencil": "Stencil" }, default="texture", label="Texture Mode" ) ] def load(self, context, name, namespace, data): asset = context['asset']['name'] namespace = namespace or unique_namespace( asset + "_", prefix="_" if asset[0].isdigit() else "", suffix="_", ) with namespaced(namespace, new=True) as namespace: # Create the nodes within the namespace nodes = { "texture": create_texture, "projection": create_projection, "stencil": create_stencil }[data.get("mode", "texture")]() file_node = cmds.ls(nodes, type="file")[0] self._apply_representation_context(context, file_node) # For ease of access for the user select all the nodes and select # the file node last so that UI shows its attributes by default cmds.select(list(nodes) + [file_node], replace=True) return containerise( name=name, namespace=namespace, nodes=nodes, context=context, loader=self.__class__.__name__ ) def update(self, container, representation): members = cmds.sets(container['objectName'], query=True) file_node = cmds.ls(members, type="file")[0] context = get_representation_context(representation) self._apply_representation_context(context, file_node) # Update representation cmds.setAttr( container["objectName"] + ".representation", str(representation["_id"]), type="string" ) def switch(self, container, representation): self.update(container, representation) def remove(self, container): members = cmds.sets(container['objectName'], query=True) cmds.lockNode(members, lock=False) cmds.delete([container['objectName']] + members) # Clean up the namespace try: cmds.namespace(removeNamespace=container['namespace'], deleteNamespaceContent=True) except RuntimeError: pass def _apply_representation_context(self, context, file_node): """Update the file node to match the context. This sets the file node's attributes for: - file path - udim tiling mode (if it is an udim tile) - use frame extension (if it is a sequence) - colorspace """ repre_context = context["representation"]["context"] has_frames = repre_context.get("frame") is not None has_udim = repre_context.get("udim") is not None # Set UV tiling mode if UDIM tiles if has_udim: cmds.setAttr(file_node + ".uvTilingMode", 3) # UDIM-tiles else: cmds.setAttr(file_node + ".uvTilingMode", 0) # off # Enable sequence if publish has `startFrame` and `endFrame` and # `startFrame != endFrame` if has_frames and self._is_sequence(context): # When enabling useFrameExtension maya automatically # connects an expression to .frameExtension to set # the current frame. However, this expression is generated # with some delay and thus it'll show a warning if frame 0 # doesn't exist because we're explicitly setting the # token. cmds.setAttr(file_node + ".useFrameExtension", True) else: cmds.setAttr(file_node + ".useFrameExtension", False) # Set the file node path attribute path = self._format_path(context) cmds.setAttr(file_node + ".fileTextureName", path, type="string") # Set colorspace colorspace = self._get_colorspace(context) if colorspace: cmds.setAttr(file_node + ".colorSpace", colorspace, type="string") else: self.log.debug("Unknown colorspace - setting colorspace skipped.") def _is_sequence(self, context): """Check whether frameStart and frameEnd are not the same.""" version = context.get("version", {}) representation = context.get("representation", {}) for doc in [representation, version]: # Frame range can be set on version or representation. # When set on representation it overrides version data. data = doc.get("data", {}) start = data.get("frameStartHandle", data.get("frameStart", None)) end = data.get("frameEndHandle", data.get("frameEnd", None)) if start is None or end is None: continue if start != end: return True else: return False return False def _get_colorspace(self, context): """Return colorspace of the file to load. Retrieves the explicit colorspace from the publish. If no colorspace data is stored with published content then project imageio settings are used to make an assumption of the colorspace based on the file rules. If no file rules match then None is returned. Returns: str or None: The colorspace of the file or None if not detected. """ # We can't apply color spaces if management is not enabled if not cmds.colorManagementPrefs(query=True, cmEnabled=True): return representation = context["representation"] colorspace_data = representation.get("data", {}).get("colorspaceData") if colorspace_data: return colorspace_data["colorspace"] # Assume colorspace from filepath based on project settings project_name = context["project"]["name"] host_name = get_current_host_name() project_settings = get_project_settings(project_name) config_data = get_imageio_config( project_name, host_name, project_settings=project_settings ) # ignore if host imageio is not enabled if not config_data: return file_rules = get_imageio_file_rules( project_name, host_name, project_settings=project_settings ) path = get_representation_path_from_context(context) colorspace = get_imageio_file_rules_colorspace_from_filepath( path, host_name, project_name, config_data=config_data, file_rules=file_rules, project_settings=project_settings ) return colorspace def _format_path(self, context): """Format the path with correct tokens for frames and udim tiles.""" context = copy.deepcopy(context) representation = context["representation"] template = representation.get("data", {}).get("template") if not template: # No template to find token locations for return get_representation_path_from_context(context) def _placeholder(key): # Substitute with a long placeholder value so that potential # custom formatting with padding doesn't find its way into # our formatting, so that wouldn't be padded as 0 return "___{}___".format(key) # We format UDIM and Frame numbers with their specific tokens. To do so # we in-place change the representation context data to format the path # with our own data tokens = { "frame": " ", "udim": " " } has_tokens = False repre_context = representation["context"] for key, _token in tokens.items(): if key in repre_context: repre_context[key] = _placeholder(key) has_tokens = True # Replace with our custom template that has the tokens set representation["data"]["template"] = template path = get_representation_path_from_context(context) if has_tokens: for key, token in tokens.items(): if key in repre_context: path = path.replace(_placeholder(key), token) return path ================================================ FILE: openpype/hosts/maya/plugins/load/load_image_plane.py ================================================ from qtpy import QtWidgets, QtCore from openpype.client import ( get_asset_by_id, get_subset_by_id, get_version_by_id, ) from openpype.pipeline import ( load, get_representation_path, get_current_project_name, ) from openpype.hosts.maya.api.pipeline import containerise from openpype.hosts.maya.api.lib import ( unique_namespace, namespaced, pairwise, get_container_members ) from maya import cmds def disconnect_inputs(plug): overrides = cmds.listConnections(plug, source=True, destination=False, plugs=True, connections=True) or [] for dest, src in pairwise(overrides): cmds.disconnectAttr(src, dest) class CameraWindow(QtWidgets.QDialog): def __init__(self, cameras): super(CameraWindow, self).__init__() self.setWindowFlags(self.windowFlags() | QtCore.Qt.FramelessWindowHint) self.camera = None self.widgets = { "label": QtWidgets.QLabel("Select camera for image plane."), "list": QtWidgets.QListWidget(), "staticImagePlane": QtWidgets.QCheckBox(), "showInAllViews": QtWidgets.QCheckBox(), "warning": QtWidgets.QLabel("No cameras selected!"), "buttons": QtWidgets.QWidget(), "okButton": QtWidgets.QPushButton("Ok"), "cancelButton": QtWidgets.QPushButton("Cancel") } # Build warning. self.widgets["warning"].setVisible(False) self.widgets["warning"].setStyleSheet("color: red") # Build list. for camera in cameras: self.widgets["list"].addItem(camera) # Build buttons. layout = QtWidgets.QHBoxLayout(self.widgets["buttons"]) layout.addWidget(self.widgets["okButton"]) layout.addWidget(self.widgets["cancelButton"]) # Build layout. layout = QtWidgets.QVBoxLayout(self) layout.addWidget(self.widgets["label"]) layout.addWidget(self.widgets["list"]) layout.addWidget(self.widgets["buttons"]) layout.addWidget(self.widgets["warning"]) self.widgets["okButton"].pressed.connect(self.on_ok_pressed) self.widgets["cancelButton"].pressed.connect(self.on_cancel_pressed) self.widgets["list"].itemPressed.connect(self.on_list_itemPressed) def on_list_itemPressed(self, item): self.camera = item.text() def on_ok_pressed(self): if self.camera is None: self.widgets["warning"].setVisible(True) return self.close() def on_cancel_pressed(self): self.camera = None self.close() class ImagePlaneLoader(load.LoaderPlugin): """Specific loader of plate for image planes on selected camera.""" families = ["image", "plate", "render"] label = "Load imagePlane" representations = ["mov", "exr", "preview", "png", "jpg"] icon = "image" color = "orange" def load(self, context, name, namespace, data, options=None): image_plane_depth = 1000 asset = context['asset']['name'] namespace = namespace or unique_namespace( asset + "_", prefix="_" if asset[0].isdigit() else "", suffix="_", ) # Get camera from user selection. # is_static_image_plane = None # is_in_all_views = None camera = data.get("camera") if data else None if not camera: cameras = cmds.ls(type="camera") # Cameras by names camera_names = {} for camera in cameras: parent = cmds.listRelatives(camera, parent=True, path=True)[0] camera_names[parent] = camera camera_names["Create new camera."] = "create-camera" window = CameraWindow(camera_names.keys()) window.exec_() # Skip if no camera was selected (Dialog was closed) if window.camera not in camera_names: return camera = camera_names[window.camera] if camera == "create-camera": camera = cmds.createNode("camera") if camera is None: return try: cmds.setAttr("{}.displayResolution".format(camera), True) cmds.setAttr("{}.farClipPlane".format(camera), image_plane_depth * 10) except RuntimeError: pass # Create image plane with namespaced(namespace): # Create inside the namespace image_plane_transform, image_plane_shape = cmds.imagePlane( fileName=context["representation"]["data"]["path"], camera=camera ) start_frame = cmds.playbackOptions(query=True, min=True) end_frame = cmds.playbackOptions(query=True, max=True) for attr, value in { "depth": image_plane_depth, "frameOffset": 0, "frameIn": start_frame, "frameOut": end_frame, "frameCache": end_frame, "useFrameExtension": True }.items(): plug = "{}.{}".format(image_plane_shape, attr) cmds.setAttr(plug, value) movie_representations = ["mov", "preview"] if context["representation"]["name"] in movie_representations: cmds.setAttr(image_plane_shape + ".type", 2) # Ask user whether to use sequence or still image. if context["representation"]["name"] == "exr": # Ensure OpenEXRLoader plugin is loaded. cmds.loadPlugin("OpenEXRLoader", quiet=True) message = ( "Hold image sequence on first frame?" "\n{} files available.".format( len(context["representation"]["files"]) ) ) reply = QtWidgets.QMessageBox.information( None, "Frame Hold.", message, QtWidgets.QMessageBox.Yes, QtWidgets.QMessageBox.No ) if reply == QtWidgets.QMessageBox.Yes: frame_extension_plug = "{}.frameExtension".format(image_plane_shape) # noqa # Remove current frame expression disconnect_inputs(frame_extension_plug) cmds.setAttr(frame_extension_plug, start_frame) new_nodes = [image_plane_transform, image_plane_shape] return containerise( name=name, namespace=namespace, nodes=new_nodes, context=context, loader=self.__class__.__name__ ) def update(self, container, representation): members = get_container_members(container) image_planes = cmds.ls(members, type="imagePlane") assert image_planes, "Image plane not found." image_plane_shape = image_planes[0] path = get_representation_path(representation) cmds.setAttr("{}.imageName".format(image_plane_shape), path, type="string") cmds.setAttr("{}.representation".format(container["objectName"]), str(representation["_id"]), type="string") # Set frame range. project_name = get_current_project_name() version = get_version_by_id( project_name, representation["parent"], fields=["parent"] ) subset = get_subset_by_id( project_name, version["parent"], fields=["parent"] ) asset = get_asset_by_id( project_name, subset["parent"], fields=["parent"] ) start_frame = asset["data"]["frameStart"] end_frame = asset["data"]["frameEnd"] for attr, value in { "frameOffset": 0, "frameIn": start_frame, "frameOut": end_frame, "frameCache": end_frame }: plug = "{}.{}".format(image_plane_shape, attr) cmds.setAttr(plug, value) def switch(self, container, representation): self.update(container, representation) def remove(self, container): members = cmds.sets(container['objectName'], query=True) cmds.lockNode(members, lock=False) cmds.delete([container['objectName']] + members) # Clean up the namespace try: cmds.namespace(removeNamespace=container['namespace'], deleteNamespaceContent=True) except RuntimeError: pass ================================================ FILE: openpype/hosts/maya/plugins/load/load_look.py ================================================ # -*- coding: utf-8 -*- """Look loader.""" import json from collections import defaultdict from qtpy import QtWidgets from openpype.client import get_representation_by_name from openpype.pipeline import ( get_current_project_name, get_representation_path, ) import openpype.hosts.maya.api.plugin from openpype.hosts.maya.api import lib from openpype.widgets.message_window import ScrollMessageBox from openpype.hosts.maya.api.lib import get_reference_node class LookLoader(openpype.hosts.maya.api.plugin.ReferenceLoader): """Specific loader for lookdev""" families = ["look"] representations = ["ma"] label = "Reference look" order = -10 icon = "code-fork" color = "orange" def process_reference(self, context, name, namespace, options): from maya import cmds with lib.maintained_selection(): file_url = self.prepare_root_value( file_url=self.filepath_from_context(context), project_name=context["project"]["name"] ) nodes = cmds.file(file_url, namespace=namespace, reference=True, returnNewNodes=True) self[:] = nodes def switch(self, container, representation): self.update(container, representation) def update(self, container, representation): """ Called by Scene Inventory when look should be updated to current version. If any reference edits cannot be applied, eg. shader renamed and material not present, reference is unloaded and cleaned. All failed edits are highlighted to the user via message box. Args: container: object that has look to be updated representation: (dict): relationship data to get proper representation from DB and persisted data in .json Returns: None """ from maya import cmds # Get reference node from container members members = lib.get_container_members(container) reference_node = get_reference_node(members, log=self.log) shader_nodes = cmds.ls(members, type='shadingEngine') orig_nodes = set(self._get_nodes_with_shader(shader_nodes)) # Trigger the regular reference update on the ReferenceLoader super(LookLoader, self).update(container, representation) # get new applied shaders and nodes from new version shader_nodes = cmds.ls(members, type='shadingEngine') nodes = set(self._get_nodes_with_shader(shader_nodes)) project_name = get_current_project_name() json_representation = get_representation_by_name( project_name, "json", representation["parent"] ) # Load relationships shader_relation = get_representation_path(json_representation) with open(shader_relation, "r") as f: json_data = json.load(f) # update of reference could result in failed edits - material is not # present because of renaming etc. If so highlight failed edits to user failed_edits = cmds.referenceQuery(reference_node, editStrings=True, failedEdits=True, successfulEdits=False) if failed_edits: # clean references - removes failed reference edits cmds.file(cr=reference_node) # cleanReference # reapply shading groups from json representation on orig nodes lib.apply_shaders(json_data, shader_nodes, orig_nodes) msg = ["During reference update some edits failed.", "All successful edits were kept intact.\n", "Failed and removed edits:"] msg.extend(failed_edits) msg = ScrollMessageBox(QtWidgets.QMessageBox.Warning, "Some reference edit failed", msg) msg.exec_() attributes = json_data.get("attributes", []) # region compute lookup nodes_by_id = defaultdict(list) for node in nodes: nodes_by_id[lib.get_id(node)].append(node) lib.apply_attributes(attributes, nodes_by_id) def _get_nodes_with_shader(self, shader_nodes): """ Returns list of nodes belonging to specific shaders Args: shader_nodes: of Shader groups Returns
node names """ from maya import cmds for shader in shader_nodes: future = cmds.listHistory(shader, future=True) connections = cmds.listConnections(future, type='mesh') if connections: # Ensure unique entries only to optimize query and results connections = list(set(connections)) return cmds.listRelatives(connections, shapes=True, fullPath=True) or [] return [] ================================================ FILE: openpype/hosts/maya/plugins/load/load_matchmove.py ================================================ # -*- coding: utf-8 -*- from maya import cmds, mel # noqa: F401 from openpype.hosts.maya.api.pipeline import containerise from openpype.hosts.maya.api import lib, Loader from openpype.pipeline.load import get_representation_path, LoadError class MatchmoveLoader(Loader): """Run matchmove script to create track in scene. Supported script types are .py and .mel TODO: there might be error in the scripts exported from 3DEqualizer that it is trying to set frame attribute on camera image plane and then add expression for image sequence. Maya will throw RuntimeError at that point that will stop processing rest of the script and the container will not be created. We should somehow handle this - maybe even by patching the mel script on-the-fly. """ families = ["matchmove"] representations = ["py", "mel"] defaults = ["Camera", "Object", "Mocap"] label = "Run matchmove script" icon = "empire" color = "orange" def load(self, context, name, namespace, options): path = self.filepath_from_context(context) custom_group_name, custom_namespace, options = \ self.get_custom_namespace_and_group( context, options, "matchmove_loader") namespace = lib.get_custom_namespace(custom_namespace) nodes = self._load_nodes_from_script(path) return containerise( name=name, namespace=namespace, nodes=nodes, context=context, loader=self.__class__.__name__ ) def update(self, container, representation): # type: (dict, dict) -> None """Update container with specified representation.""" self.remove(container) path = get_representation_path(representation) namespace = container["namespace"] print(f">>> loading from {path}") try: nodes = self._load_nodes_from_script(path) except RuntimeError as e: raise LoadError("Failed to load matchmove script.") from e return containerise( name=container["name"], namespace=namespace, nodes=nodes, context=representation["context"], loader=self.__class__.__name__ ) def switch(self, container, representation): self.update(container, representation) def remove(self, container): """Delete container and its contents.""" if cmds.objExists(container['objectName']): members = cmds.sets(container['objectName'], query=True) or [] cmds.delete([container['objectName']] + members) def _load_nodes_from_script(self, path): # type: (str) -> list """Load nodes from script. This will execute py or mel script and resulting nodes will be returned. Args: path (str): path to script Returns: list: list of created nodes """ previous_nodes = set(cmds.ls(long=True)) if path.lower().endswith(".py"): exec(open(path).read()) elif path.lower().endswith(".mel"): mel.eval(open(path).read()) else: self.log.error("Unsupported script type") current_nodes = set(cmds.ls(long=True)) return list(current_nodes - previous_nodes) ================================================ FILE: openpype/hosts/maya/plugins/load/load_maya_usd.py ================================================ # -*- coding: utf-8 -*- import maya.cmds as cmds from openpype.pipeline import ( load, get_representation_path, ) from openpype.pipeline.load import get_representation_path_from_context from openpype.hosts.maya.api.lib import ( namespaced, unique_namespace ) from openpype.hosts.maya.api.pipeline import containerise class MayaUsdLoader(load.LoaderPlugin): """Read USD data in a Maya USD Proxy""" families = ["model", "usd", "pointcache", "animation"] representations = ["usd", "usda", "usdc", "usdz", "abc"] label = "Load USD to Maya Proxy" order = -1 icon = "code-fork" color = "orange" def load(self, context, name=None, namespace=None, options=None): asset = context['asset']['name'] namespace = namespace or unique_namespace( asset + "_", prefix="_" if asset[0].isdigit() else "", suffix="_", ) # Make sure we can load the plugin cmds.loadPlugin("mayaUsdPlugin", quiet=True) path = get_representation_path_from_context(context) # Create the shape cmds.namespace(addNamespace=namespace) with namespaced(namespace, new=False): transform = cmds.createNode("transform", name=name, skipSelect=True) proxy = cmds.createNode('mayaUsdProxyShape', name="{}Shape".format(name), parent=transform, skipSelect=True) cmds.connectAttr("time1.outTime", "{}.time".format(proxy)) cmds.setAttr("{}.filePath".format(proxy), path, type="string") # By default, we force the proxy to not use a shared stage because # when doing so Maya will quite easily allow to save into the # loaded usd file. Since we are loading published files we want to # avoid altering them. Unshared stages also save their edits into # the workfile as an artist might expect it to do. cmds.setAttr("{}.shareStage".format(proxy), False) # cmds.setAttr("{}.shareStage".format(proxy), lock=True) nodes = [transform, proxy] self[:] = nodes return containerise( name=name, namespace=namespace, nodes=nodes, context=context, loader=self.__class__.__name__) def update(self, container, representation): # type: (dict, dict) -> None """Update container with specified representation.""" node = container['objectName'] assert cmds.objExists(node), "Missing container" members = cmds.sets(node, query=True) or [] shapes = cmds.ls(members, type="mayaUsdProxyShape") path = get_representation_path(representation) for shape in shapes: cmds.setAttr("{}.filePath".format(shape), path, type="string") cmds.setAttr("{}.representation".format(node), str(representation["_id"]), type="string") def switch(self, container, representation): self.update(container, representation) def remove(self, container): # type: (dict) -> None """Remove loaded container.""" # Delete container and its contents if cmds.objExists(container['objectName']): members = cmds.sets(container['objectName'], query=True) or [] cmds.delete([container['objectName']] + members) # Remove the namespace, if empty namespace = container['namespace'] if cmds.namespace(exists=namespace): members = cmds.namespaceInfo(namespace, listNamespace=True) if not members: cmds.namespace(removeNamespace=namespace) else: self.log.warning("Namespace not deleted because it " "still has members: %s", namespace) ================================================ FILE: openpype/hosts/maya/plugins/load/load_multiverse_usd.py ================================================ # -*- coding: utf-8 -*- import maya.cmds as cmds from maya import mel import os from openpype.pipeline import ( load, get_representation_path ) from openpype.hosts.maya.api.lib import ( maintained_selection, namespaced, unique_namespace ) from openpype.hosts.maya.api.pipeline import containerise from openpype.client import get_representation_by_id class MultiverseUsdLoader(load.LoaderPlugin): """Read USD data in a Multiverse Compound""" families = ["model", "usd", "mvUsdComposition", "mvUsdOverride", "pointcache", "animation"] representations = ["usd", "usda", "usdc", "usdz", "abc"] label = "Load USD to Multiverse" order = -10 icon = "code-fork" color = "orange" def load(self, context, name=None, namespace=None, options=None): asset = context['asset']['name'] namespace = namespace or unique_namespace( asset + "_", prefix="_" if asset[0].isdigit() else "", suffix="_", ) path = self.filepath_from_context(context) # Make sure we can load the plugin cmds.loadPlugin("MultiverseForMaya", quiet=True) import multiverse # Create the shape with maintained_selection(): cmds.namespace(addNamespace=namespace) with namespaced(namespace, new=False): shape = multiverse.CreateUsdCompound(path) transform = cmds.listRelatives( shape, parent=True, fullPath=True)[0] nodes = [transform, shape] self[:] = nodes return containerise( name=name, namespace=namespace, nodes=nodes, context=context, loader=self.__class__.__name__) def update(self, container, representation): # type: (dict, dict) -> None """Update container with specified representation.""" node = container['objectName'] assert cmds.objExists(node), "Missing container" members = cmds.sets(node, query=True) or [] shapes = cmds.ls(members, type="mvUsdCompoundShape") assert shapes, "Cannot find mvUsdCompoundShape in container" project_name = representation["context"]["project"]["name"] prev_representation_id = cmds.getAttr("{}.representation".format(node)) prev_representation = get_representation_by_id(project_name, prev_representation_id) prev_path = os.path.normpath(prev_representation["data"]["path"]) # Make sure we can load the plugin cmds.loadPlugin("MultiverseForMaya", quiet=True) import multiverse for shape in shapes: asset_paths = multiverse.GetUsdCompoundAssetPaths(shape) asset_paths = [os.path.normpath(p) for p in asset_paths] assert asset_paths.count(prev_path) == 1, \ "Couldn't find matching path (or too many)" prev_path_idx = asset_paths.index(prev_path) path = get_representation_path(representation) asset_paths[prev_path_idx] = path multiverse.SetUsdCompoundAssetPaths(shape, asset_paths) cmds.setAttr("{}.representation".format(node), str(representation["_id"]), type="string") mel.eval('refreshEditorTemplates;') def switch(self, container, representation): self.update(container, representation) def remove(self, container): # type: (dict) -> None """Remove loaded container.""" # Delete container and its contents if cmds.objExists(container['objectName']): members = cmds.sets(container['objectName'], query=True) or [] cmds.delete([container['objectName']] + members) # Remove the namespace, if empty namespace = container['namespace'] if cmds.namespace(exists=namespace): members = cmds.namespaceInfo(namespace, listNamespace=True) if not members: cmds.namespace(removeNamespace=namespace) else: self.log.warning("Namespace not deleted because it " "still has members: %s", namespace) ================================================ FILE: openpype/hosts/maya/plugins/load/load_multiverse_usd_over.py ================================================ # -*- coding: utf-8 -*- import maya.cmds as cmds from maya import mel import os import qargparse from openpype.pipeline import ( load, get_representation_path ) from openpype.hosts.maya.api.lib import ( maintained_selection ) from openpype.hosts.maya.api.pipeline import containerise from openpype.client import get_representation_by_id class MultiverseUsdOverLoader(load.LoaderPlugin): """Reference file""" families = ["mvUsdOverride"] representations = ["usda", "usd", "udsz"] label = "Load Usd Override into Compound" order = -10 icon = "code-fork" color = "orange" options = [ qargparse.String( "Which Compound", label="Compound", help="Select which compound to add this as a layer to." ) ] def load(self, context, name=None, namespace=None, options=None): current_usd = cmds.ls(selection=True, type="mvUsdCompoundShape", dag=True, long=True) if len(current_usd) != 1: self.log.error("Current selection invalid: '{}', " "must contain exactly 1 mvUsdCompoundShape." "".format(current_usd)) return # Make sure we can load the plugin cmds.loadPlugin("MultiverseForMaya", quiet=True) import multiverse path = self.filepath_from_context(context) nodes = current_usd with maintained_selection(): multiverse.AddUsdCompoundAssetPath(current_usd[0], path) namespace = current_usd[0].split("|")[1].split(":")[0] container = containerise( name=name, namespace=namespace, nodes=nodes, context=context, loader=self.__class__.__name__) cmds.addAttr(container, longName="mvUsdCompoundShape", niceName="mvUsdCompoundShape", dataType="string") cmds.setAttr(container + ".mvUsdCompoundShape", current_usd[0], type="string") return container def update(self, container, representation): # type: (dict, dict) -> None """Update container with specified representation.""" cmds.loadPlugin("MultiverseForMaya", quiet=True) import multiverse node = container['objectName'] assert cmds.objExists(node), "Missing container" members = cmds.sets(node, query=True) or [] shapes = cmds.ls(members, type="mvUsdCompoundShape") assert shapes, "Cannot find mvUsdCompoundShape in container" mvShape = container['mvUsdCompoundShape'] assert mvShape, "Missing mv source" project_name = representation["context"]["project"]["name"] prev_representation_id = cmds.getAttr("{}.representation".format(node)) prev_representation = get_representation_by_id(project_name, prev_representation_id) prev_path = os.path.normpath(prev_representation["data"]["path"]) path = get_representation_path(representation) for shape in shapes: asset_paths = multiverse.GetUsdCompoundAssetPaths(shape) asset_paths = [os.path.normpath(p) for p in asset_paths] assert asset_paths.count(prev_path) == 1, \ "Couldn't find matching path (or too many)" prev_path_idx = asset_paths.index(prev_path) asset_paths[prev_path_idx] = path multiverse.SetUsdCompoundAssetPaths(shape, asset_paths) cmds.setAttr("{}.representation".format(node), str(representation["_id"]), type="string") mel.eval('refreshEditorTemplates;') def switch(self, container, representation): self.update(container, representation) def remove(self, container): # type: (dict) -> None """Remove loaded container.""" # Delete container and its contents if cmds.objExists(container['objectName']): members = cmds.sets(container['objectName'], query=True) or [] cmds.delete([container['objectName']] + members) # Remove the namespace, if empty namespace = container['namespace'] if cmds.namespace(exists=namespace): members = cmds.namespaceInfo(namespace, listNamespace=True) if not members: cmds.namespace(removeNamespace=namespace) else: self.log.warning("Namespace not deleted because it " "still has members: %s", namespace) ================================================ FILE: openpype/hosts/maya/plugins/load/load_redshift_proxy.py ================================================ # -*- coding: utf-8 -*- """Loader for Redshift proxy.""" import os import clique import maya.cmds as cmds from openpype.settings import get_project_settings from openpype.pipeline import ( load, get_representation_path ) from openpype.hosts.maya.api.lib import ( namespaced, maintained_selection, unique_namespace ) from openpype.hosts.maya.api.pipeline import containerise class RedshiftProxyLoader(load.LoaderPlugin): """Load Redshift proxy""" families = ["redshiftproxy"] representations = ["rs"] label = "Import Redshift Proxy" order = -10 icon = "code-fork" color = "orange" def load(self, context, name=None, namespace=None, options=None): """Plugin entry point.""" try: family = context["representation"]["context"]["family"] except ValueError: family = "redshiftproxy" asset_name = context['asset']["name"] namespace = namespace or unique_namespace( asset_name + "_", prefix="_" if asset_name[0].isdigit() else "", suffix="_", ) # Ensure Redshift for Maya is loaded. cmds.loadPlugin("redshift4maya", quiet=True) path = self.filepath_from_context(context) with maintained_selection(): cmds.namespace(addNamespace=namespace) with namespaced(namespace, new=False): nodes, group_node = self.create_rs_proxy(name, path) self[:] = nodes if not nodes: return # colour the group node project_name = context["project"]["name"] settings = get_project_settings(project_name) colors = settings['maya']['load']['colors'] c = colors.get(family) if c is not None: cmds.setAttr("{0}.useOutlinerColor".format(group_node), 1) cmds.setAttr("{0}.outlinerColor".format(group_node), c[0], c[1], c[2]) return containerise( name=name, namespace=namespace, nodes=nodes, context=context, loader=self.__class__.__name__) def update(self, container, representation): node = container['objectName'] assert cmds.objExists(node), "Missing container" members = cmds.sets(node, query=True) or [] rs_meshes = cmds.ls(members, type="RedshiftProxyMesh") assert rs_meshes, "Cannot find RedshiftProxyMesh in container" filename = get_representation_path(representation) for rs_mesh in rs_meshes: cmds.setAttr("{}.fileName".format(rs_mesh), filename, type="string") # Update metadata cmds.setAttr("{}.representation".format(node), str(representation["_id"]), type="string") def remove(self, container): # Delete container and its contents if cmds.objExists(container['objectName']): members = cmds.sets(container['objectName'], query=True) or [] cmds.delete([container['objectName']] + members) # Remove the namespace, if empty namespace = container['namespace'] if cmds.namespace(exists=namespace): members = cmds.namespaceInfo(namespace, listNamespace=True) if not members: cmds.namespace(removeNamespace=namespace) else: self.log.warning("Namespace not deleted because it " "still has members: %s", namespace) def switch(self, container, representation): self.update(container, representation) def create_rs_proxy(self, name, path): """Creates Redshift Proxies showing a proxy object. Args: name (str): Proxy name. path (str): Path to proxy file. Returns: (str, str): Name of mesh with Redshift proxy and its parent transform. """ rs_mesh = cmds.createNode( 'RedshiftProxyMesh', name="{}_RS".format(name)) mesh_shape = cmds.createNode("mesh", name="{}_GEOShape".format(name)) cmds.setAttr("{}.fileName".format(rs_mesh), path, type="string") cmds.connectAttr("{}.outMesh".format(rs_mesh), "{}.inMesh".format(mesh_shape)) # TODO: use the assigned shading group as shaders if existed # assign default shader to redshift proxy if cmds.ls("initialShadingGroup", type="shadingEngine"): cmds.sets(mesh_shape, forceElement="initialShadingGroup") group_node = cmds.group(empty=True, name="{}_GRP".format(name)) mesh_transform = cmds.listRelatives(mesh_shape, parent=True, fullPath=True) cmds.parent(mesh_transform, group_node) nodes = [rs_mesh, mesh_shape, group_node] # determine if we need to enable animation support files_in_folder = os.listdir(os.path.dirname(path)) collections, remainder = clique.assemble(files_in_folder) if collections: cmds.setAttr("{}.useFrameExtension".format(rs_mesh), 1) return nodes, group_node ================================================ FILE: openpype/hosts/maya/plugins/load/load_reference.py ================================================ import os import difflib import contextlib from maya import cmds import qargparse from openpype.settings import get_project_settings import openpype.hosts.maya.api.plugin from openpype.hosts.maya.api.lib import ( maintained_selection, get_container_members, parent_nodes, create_rig_animation_instance ) @contextlib.contextmanager def preserve_modelpanel_cameras(container, log=None): """Preserve camera members of container in the modelPanels. This is used to ensure a camera remains in the modelPanels after updating to a new version. """ # Get the modelPanels that used the old camera members = get_container_members(container) old_cameras = set(cmds.ls(members, type="camera", long=True)) if not old_cameras: # No need to manage anything yield return panel_cameras = {} for panel in cmds.getPanel(type="modelPanel"): cam = cmds.ls(cmds.modelPanel(panel, query=True, camera=True), long=True)[0] # Often but not always maya returns the transform from the # modelPanel as opposed to the camera shape, so we convert it # to explicitly be the camera shape if cmds.nodeType(cam) != "camera": cam = cmds.listRelatives(cam, children=True, fullPath=True, type="camera")[0] if cam in old_cameras: panel_cameras[panel] = cam if not panel_cameras: # No need to manage anything yield return try: yield finally: new_members = get_container_members(container) new_cameras = set(cmds.ls(new_members, type="camera", long=True)) if not new_cameras: return for panel, cam_name in panel_cameras.items(): new_camera = None if cam_name in new_cameras: new_camera = cam_name elif len(new_cameras) == 1: new_camera = next(iter(new_cameras)) else: # Multiple cameras in the updated container but not an exact # match detected by name. Find the closest match matches = difflib.get_close_matches(word=cam_name, possibilities=new_cameras, n=1) if matches: new_camera = matches[0] # best match if log: log.info("Camera in '{}' restored with " "closest match camera: {} (before: {})" .format(panel, new_camera, cam_name)) if not new_camera: # Unable to find the camera to re-apply in the modelpanel continue cmds.modelPanel(panel, edit=True, camera=new_camera) class ReferenceLoader(openpype.hosts.maya.api.plugin.ReferenceLoader): """Reference file""" families = ["model", "pointcache", "proxyAbc", "animation", "mayaAscii", "mayaScene", "setdress", "layout", "camera", "rig", "camerarig", "staticMesh", "skeletalMesh", "mvLook", "matchmove"] representations = ["ma", "abc", "fbx", "mb"] label = "Reference" order = -10 icon = "code-fork" color = "orange" def process_reference(self, context, name, namespace, options): import maya.cmds as cmds try: family = context["representation"]["context"]["family"] except ValueError: family = "model" project_name = context["project"]["name"] # True by default to keep legacy behaviours attach_to_root = options.get("attach_to_root", True) group_name = options["group_name"] # no group shall be created if not attach_to_root: group_name = namespace kwargs = {} if "file_options" in options: kwargs["options"] = options["file_options"] if "file_type" in options: kwargs["type"] = options["file_type"] path = self.filepath_from_context(context) with maintained_selection(): cmds.loadPlugin("AbcImport.mll", quiet=True) file_url = self.prepare_root_value(path, project_name) nodes = cmds.file(file_url, namespace=namespace, sharedReferenceFile=False, reference=True, returnNewNodes=True, groupReference=attach_to_root, groupName=group_name, **kwargs) shapes = cmds.ls(nodes, shapes=True, long=True) new_nodes = (list(set(nodes) - set(shapes))) # if there are cameras, try to lock their transforms self._lock_camera_transforms(new_nodes) current_namespace = cmds.namespaceInfo(currentNamespace=True) if current_namespace != ":": group_name = current_namespace + ":" + group_name self[:] = new_nodes if attach_to_root: group_name = "|" + group_name roots = cmds.listRelatives(group_name, children=True, fullPath=True) or [] if family not in {"layout", "setdress", "mayaAscii", "mayaScene"}: # QUESTION Why do we need to exclude these families? with parent_nodes(roots, parent=None): cmds.xform(group_name, zeroTransformPivots=True) settings = get_project_settings(project_name) display_handle = settings['maya']['load'].get( 'reference_loader', {} ).get('display_handle', True) cmds.setAttr( "{}.displayHandle".format(group_name), display_handle ) colors = settings['maya']['load']['colors'] c = colors.get(family) if c is not None: cmds.setAttr("{}.useOutlinerColor".format(group_name), 1) cmds.setAttr("{}.outlinerColor".format(group_name), (float(c[0]) / 255), (float(c[1]) / 255), (float(c[2]) / 255)) cmds.setAttr( "{}.displayHandle".format(group_name), display_handle ) # get bounding box bbox = cmds.exactWorldBoundingBox(group_name) # get pivot position on world space pivot = cmds.xform(group_name, q=True, sp=True, ws=True) # center of bounding box cx = (bbox[0] + bbox[3]) / 2 cy = (bbox[1] + bbox[4]) / 2 cz = (bbox[2] + bbox[5]) / 2 # add pivot position to calculate offset cx = cx + pivot[0] cy = cy + pivot[1] cz = cz + pivot[2] # set selection handle offset to center of bounding box cmds.setAttr("{}.selectHandleX".format(group_name), cx) cmds.setAttr("{}.selectHandleY".format(group_name), cy) cmds.setAttr("{}.selectHandleZ".format(group_name), cz) if family == "rig": self._post_process_rig(namespace, context, options) else: if "translate" in options: if not attach_to_root and new_nodes: root_nodes = cmds.ls(new_nodes, assemblies=True, long=True) # we assume only a single root is ever loaded group_name = root_nodes[0] cmds.setAttr("{}.translate".format(group_name), *options["translate"]) return new_nodes def switch(self, container, representation): self.update(container, representation) def update(self, container, representation): with preserve_modelpanel_cameras(container, log=self.log): super(ReferenceLoader, self).update(container, representation) # We also want to lock camera transforms on any new cameras in the # reference or for a camera which might have changed names. members = get_container_members(container) self._lock_camera_transforms(members) def _post_process_rig(self, namespace, context, options): nodes = self[:] create_rig_animation_instance( nodes, context, namespace, options=options, log=self.log ) def _lock_camera_transforms(self, nodes): cameras = cmds.ls(nodes, type="camera") if not cameras: return # Check the Maya version, lockTransform has been introduced since # Maya 2016.5 Ext 2 version = int(cmds.about(version=True)) if version >= 2016: for camera in cameras: cmds.camera(camera, edit=True, lockTransform=True) else: self.log.warning("This version of Maya does not support locking of" " transforms of cameras.") class MayaUSDReferenceLoader(ReferenceLoader): """Reference USD file to native Maya nodes using MayaUSDImport reference""" label = "Reference Maya USD" families = ["usd"] representations = ["usd"] extensions = {"usd", "usda", "usdc"} options = ReferenceLoader.options + [ qargparse.Boolean( "readAnimData", label="Load anim data", default=True, help="Load animation data from USD file" ), qargparse.Boolean( "useAsAnimationCache", label="Use as animation cache", default=True, help=( "Imports geometry prims with time-sampled point data using a " "point-based deformer that references the imported " "USD file.\n" "This provides better import and playback performance when " "importing time-sampled geometry from USD, and should " "reduce the weight of the resulting Maya scene." ) ), qargparse.Boolean( "importInstances", label="Import instances", default=True, help=( "Import USD instanced geometries as Maya instanced shapes. " "Will flatten the scene otherwise." ) ), qargparse.String( "primPath", label="Prim Path", default="/", help=( "Name of the USD scope where traversing will begin.\n" "The prim at the specified primPath (including the prim) will " "be imported.\n" "Specifying the pseudo-root (/) means you want " "to import everything in the file.\n" "If the passed prim path is empty, it will first try to " "import the defaultPrim for the rootLayer if it exists.\n" "Otherwise, it will behave as if the pseudo-root was passed " "in." ) ) ] file_type = "USD Import" def process_reference(self, context, name, namespace, options): cmds.loadPlugin("mayaUsdPlugin", quiet=True) def bool_option(key, default): # Shorthand for getting optional boolean file option from options value = int(bool(options.get(key, default))) return "{}={}".format(key, value) def string_option(key, default): # Shorthand for getting optional string file option from options value = str(options.get(key, default)) return "{}={}".format(key, value) options["file_options"] = ";".join([ string_option("primPath", default="/"), bool_option("importInstances", default=True), bool_option("useAsAnimationCache", default=True), bool_option("readAnimData", default=True), # TODO: Expose more parameters # "preferredMaterial=none", # "importRelativeTextures=Automatic", # "useCustomFrameRange=0", # "startTime=0", # "endTime=0", # "importUSDZTextures=0" ]) options["file_type"] = self.file_type return super(MayaUSDReferenceLoader, self).process_reference( context, name, namespace, options ) ================================================ FILE: openpype/hosts/maya/plugins/load/load_rendersetup.py ================================================ # -*- coding: utf-8 -*- """Load and update RenderSetup settings. Working with RenderSetup setting is Maya is done utilizing json files. When this json is loaded, it will overwrite all settings on RenderSetup instance. """ import json import sys import six from openpype.pipeline import ( load, get_representation_path ) from openpype.hosts.maya.api import lib from openpype.hosts.maya.api.pipeline import containerise from maya import cmds import maya.app.renderSetup.model.renderSetup as renderSetup class RenderSetupLoader(load.LoaderPlugin): """Load json preset for RenderSetup overwriting current one.""" families = ["rendersetup"] representations = ["json"] defaults = ['Main'] label = "Load RenderSetup template" icon = "tablet" color = "orange" def load(self, context, name, namespace, data): """Load RenderSetup settings.""" # from openpype.hosts.maya.api.lib import namespaced asset = context['asset']['name'] namespace = namespace or lib.unique_namespace( asset + "_", prefix="_" if asset[0].isdigit() else "", suffix="_", ) path = self.filepath_from_context(context) self.log.info(">>> loading json [ {} ]".format(path)) with open(path, "r") as file: renderSetup.instance().decode( json.load(file), renderSetup.DECODE_AND_OVERWRITE, None) nodes = [] null = cmds.sets(name="null_SET", empty=True) nodes.append(null) self[:] = nodes if not nodes: return self.log.info(">>> containerising [ {} ]".format(name)) return containerise( name=name, namespace=namespace, nodes=nodes, context=context, loader=self.__class__.__name__) def remove(self, container): """Remove RenderSetup settings instance.""" from maya import cmds container_name = container["objectName"] self.log.info("Removing '%s' from Maya.." % container["name"]) container_content = cmds.sets(container_name, query=True) nodes = cmds.ls(container_content, long=True) nodes.append(container_name) try: cmds.delete(nodes) except ValueError: # Already implicitly deleted by Maya upon removing reference pass def update(self, container, representation): """Update RenderSetup setting by overwriting existing settings.""" lib.show_message( "Render setup update", "Render setup setting will be overwritten by new version. All " "setting specified by user not included in loaded version " "will be lost.") path = get_representation_path(representation) with open(path, "r") as file: try: renderSetup.instance().decode( json.load(file), renderSetup.DECODE_AND_OVERWRITE, None) except Exception: self.log.error("There were errors during loading") six.reraise(*sys.exc_info()) # Update metadata node = container["objectName"] cmds.setAttr("{}.representation".format(node), str(representation["_id"]), type="string") self.log.info("... updated") def switch(self, container, representation): """Switch representations.""" self.update(container, representation) ================================================ FILE: openpype/hosts/maya/plugins/load/load_vdb_to_arnold.py ================================================ import os from openpype.settings import get_project_settings from openpype.pipeline import ( load, get_representation_path ) # TODO aiVolume doesn't automatically set velocity fps correctly, set manual? class LoadVDBtoArnold(load.LoaderPlugin): """Load OpenVDB for Arnold in aiVolume""" families = ["vdbcache"] representations = ["vdb"] label = "Load VDB to Arnold" icon = "cloud" color = "orange" def load(self, context, name, namespace, data): from maya import cmds from openpype.hosts.maya.api.pipeline import containerise from openpype.hosts.maya.api.lib import unique_namespace try: family = context["representation"]["context"]["family"] except ValueError: family = "vdbcache" # Check if the plugin for arnold is available on the pc try: cmds.loadPlugin("mtoa", quiet=True) except Exception as exc: self.log.error("Encountered exception:\n%s" % exc) return asset = context['asset'] asset_name = asset["name"] namespace = namespace or unique_namespace( asset_name + "_", prefix="_" if asset_name[0].isdigit() else "", suffix="_", ) # Root group label = "{}:{}".format(namespace, name) root = cmds.group(name=label, empty=True) project_name = context["project"]["name"] settings = get_project_settings(project_name) colors = settings['maya']['load']['colors'] c = colors.get(family) if c is not None: cmds.setAttr(root + ".useOutlinerColor", 1) cmds.setAttr(root + ".outlinerColor", (float(c[0]) / 255), (float(c[1]) / 255), (float(c[2]) / 255) ) # Create VRayVolumeGrid grid_node = cmds.createNode("aiVolume", name="{}Shape".format(root), parent=root) path = self.filepath_from_context(context) self._set_path(grid_node, path=path, representation=context["representation"]) # Lock the shape node so the user can't delete the transform/shape # as if it was referenced cmds.lockNode(grid_node, lock=True) nodes = [root, grid_node] self[:] = nodes return containerise( name=name, namespace=namespace, nodes=nodes, context=context, loader=self.__class__.__name__) def update(self, container, representation): from maya import cmds path = get_representation_path(representation) # Find VRayVolumeGrid members = cmds.sets(container['objectName'], query=True) grid_nodes = cmds.ls(members, type="aiVolume", long=True) assert len(grid_nodes) == 1, "This is a bug" # Update the VRayVolumeGrid self._set_path(grid_nodes[0], path=path, representation=representation) # Update container representation cmds.setAttr(container["objectName"] + ".representation", str(representation["_id"]), type="string") def switch(self, container, representation): self.update(container, representation) def remove(self, container): from maya import cmds # Get all members of the avalon container, ensure they are unlocked # and delete everything members = cmds.sets(container['objectName'], query=True) cmds.lockNode(members, lock=False) cmds.delete([container['objectName']] + members) # Clean up the namespace try: cmds.namespace(removeNamespace=container['namespace'], deleteNamespaceContent=True) except RuntimeError: pass @staticmethod def _set_path(grid_node, path, representation): """Apply the settings for the VDB path to the aiVolume node""" from maya import cmds if not os.path.exists(path): raise RuntimeError("Path does not exist: %s" % path) is_sequence = bool(representation["context"].get("frame")) cmds.setAttr(grid_node + ".useFrameExtension", is_sequence) # Set file path cmds.setAttr(grid_node + ".filename", path, type="string") ================================================ FILE: openpype/hosts/maya/plugins/load/load_vdb_to_redshift.py ================================================ import os from openpype.settings import get_project_settings from openpype.pipeline import ( load, get_representation_path ) class LoadVDBtoRedShift(load.LoaderPlugin): """Load OpenVDB in a Redshift Volume Shape Note that the RedshiftVolumeShape is created without a RedshiftVolume shader assigned. To get the Redshift volume to render correctly assign a RedshiftVolume shader (in the Hypershade) and set the density, scatter and emission channels to the channel names of the volumes in the VDB file. """ families = ["vdbcache"] representations = ["vdb"] label = "Load VDB to RedShift" icon = "cloud" color = "orange" def load(self, context, name=None, namespace=None, data=None): from maya import cmds from openpype.hosts.maya.api.pipeline import containerise from openpype.hosts.maya.api.lib import unique_namespace try: family = context["representation"]["context"]["family"] except ValueError: family = "vdbcache" # Check if the plugin for redshift is available on the pc try: cmds.loadPlugin("redshift4maya", quiet=True) except Exception as exc: self.log.error("Encountered exception:\n%s" % exc) return # Check if viewport drawing engine is Open GL Core (compat) render_engine = None compatible = "OpenGL" if cmds.optionVar(exists="vp2RenderingEngine"): render_engine = cmds.optionVar(query="vp2RenderingEngine") if not render_engine or not render_engine.startswith(compatible): raise RuntimeError("Current scene's settings are incompatible." "See Preferences > Display > Viewport 2.0 to " "set the render engine to '%s
'" % compatible) asset = context['asset'] asset_name = asset["name"] namespace = namespace or unique_namespace( asset_name + "_", prefix="_" if asset_name[0].isdigit() else "", suffix="_", ) # Root group label = "{}:{}".format(namespace, name) root = cmds.createNode("transform", name=label) project_name = context["project"]["name"] settings = get_project_settings(project_name) colors = settings['maya']['load']['colors'] c = colors.get(family) if c is not None: cmds.setAttr(root + ".useOutlinerColor", 1) cmds.setAttr(root + ".outlinerColor", (float(c[0])/255), (float(c[1])/255), (float(c[2])/255) ) # Create VR volume_node = cmds.createNode("RedshiftVolumeShape", name="{}RVSShape".format(label), parent=root) self._set_path(volume_node, path=self.filepath_from_context(context), representation=context["representation"]) nodes = [root, volume_node] self[:] = nodes return containerise( name=name, namespace=namespace, nodes=nodes, context=context, loader=self.__class__.__name__) def update(self, container, representation): from maya import cmds path = get_representation_path(representation) # Find VRayVolumeGrid members = cmds.sets(container['objectName'], query=True) grid_nodes = cmds.ls(members, type="RedshiftVolumeShape", long=True) assert len(grid_nodes) == 1, "This is a bug" # Update the VRayVolumeGrid self._set_path(grid_nodes[0], path=path, representation=representation) # Update container representation cmds.setAttr(container["objectName"] + ".representation", str(representation["_id"]), type="string") def remove(self, container): from maya import cmds # Get all members of the avalon container, ensure they are unlocked # and delete everything members = cmds.sets(container['objectName'], query=True) cmds.lockNode(members, lock=False) cmds.delete([container['objectName']] + members) # Clean up the namespace try: cmds.namespace(removeNamespace=container['namespace'], deleteNamespaceContent=True) except RuntimeError: pass def switch(self, container, representation): self.update(container, representation) @staticmethod def _set_path(grid_node, path, representation): """Apply the settings for the VDB path to the RedshiftVolumeShape""" from maya import cmds if not os.path.exists(path): raise RuntimeError("Path does not exist: %s" % path) is_sequence = bool(representation["context"].get("frame")) cmds.setAttr(grid_node + ".useFrameExtension", is_sequence) # Set file path cmds.setAttr(grid_node + ".fileName", path, type="string") ================================================ FILE: openpype/hosts/maya/plugins/load/load_vdb_to_vray.py ================================================ import os from openpype.settings import get_project_settings from openpype.pipeline import ( load, get_representation_path ) from maya import cmds # List of 3rd Party Channels Mapping names for VRayVolumeGrid # See: https://docs.chaosgroup.com/display/VRAY4MAYA/Input # #Input-3rdPartyChannelsMapping THIRD_PARTY_CHANNELS = { 2: "Smoke", 1: "Temperature", 10: "Fuel", 4: "Velocity.x", 5: "Velocity.y", 6: "Velocity.z", 7: "Red", 8: "Green", 9: "Blue", 14: "Wavelet Energy", 19: "Wavelet.u", 20: "Wavelet.v", 21: "Wavelet.w", # These are not in UI or documentation but V-Ray does seem to set these. 15: "AdvectionOrigin.x", 16: "AdvectionOrigin.y", 17: "AdvectionOrigin.z", } def _fix_duplicate_vvg_callbacks(): """Workaround to kill duplicate VRayVolumeGrids attribute callbacks. This fixes a huge lag in Maya on switching 3rd Party Channels Mappings or to different .vdb file paths because it spams an attribute changed callback: `vvgUserChannelMappingsUpdateUI`. ChaosGroup bug ticket: 154-008-9890 Found with: - Maya 2019.2 on Windows 10 - V-Ray: V-Ray Next for Maya, update 1 version 4.12.01.00001 Bug still present in: - Maya 2022.1 on Windows 10 - V-Ray 5 for Maya, Update 2.1 (v5.20.01 from Dec 16 2021) """ # todo(roy): Remove when new V-Ray release fixes duplicate calls jobs = cmds.scriptJob(listJobs=True) matched = set() for entry in jobs: # Remove the number index, callback = entry.split(":", 1) callback = callback.strip() # Detect whether it is a `vvgUserChannelMappingsUpdateUI` # attribute change callback if callback.startswith('"-runOnce" 1 "-attributeChange" "'): if '"vvgUserChannelMappingsUpdateUI(' in callback: if callback in matched: # If we've seen this callback before then # delete the duplicate callback cmds.scriptJob(kill=int(index)) else: matched.add(callback) class LoadVDBtoVRay(load.LoaderPlugin): """Load OpenVDB in a V-Ray Volume Grid""" families = ["vdbcache"] representations = ["vdb"] label = "Load VDB to VRay" icon = "cloud" color = "orange" def load(self, context, name, namespace, data): from openpype.hosts.maya.api.lib import unique_namespace from openpype.hosts.maya.api.pipeline import containerise path = self.filepath_from_context(context) assert os.path.exists(path), ( "Path does not exist: %s" % path ) try: family = context["representation"]["context"]["family"] except ValueError: family = "vdbcache" # Ensure V-ray is loaded with the vrayvolumegrid if not cmds.pluginInfo("vrayformaya", query=True, loaded=True): cmds.loadPlugin("vrayformaya") if not cmds.pluginInfo("vrayvolumegrid", query=True, loaded=True): cmds.loadPlugin("vrayvolumegrid") # Check if viewport drawing engine is Open GL Core (compat) render_engine = None compatible = "OpenGLCoreProfileCompat" if cmds.optionVar(exists="vp2RenderingEngine"): render_engine = cmds.optionVar(query="vp2RenderingEngine") if not render_engine or render_engine != compatible: self.log.warning("Current scene's settings are incompatible." "See Preferences > Display > Viewport 2.0 to " "set the render engine to '%s'" % compatible) asset = context['asset'] asset_name = asset["name"] namespace = namespace or unique_namespace( asset_name + "_", prefix="_" if asset_name[0].isdigit() else "", suffix="_", ) # Root group label = "{}:{}_VDB".format(namespace, name) root = cmds.group(name=label, empty=True) project_name = context["project"]["name"] settings = get_project_settings(project_name) colors = settings['maya']['load']['colors'] c = colors.get(family) if c is not None: cmds.setAttr(root + ".useOutlinerColor", 1) cmds.setAttr(root + ".outlinerColor", float(c[0]) / 255, float(c[1]) / 255, float(c[2]) / 255) # Create VRayVolumeGrid grid_node = cmds.createNode("VRayVolumeGrid", name="{}Shape".format(label), parent=root) # Ensure .currentTime is connected to time1.outTime cmds.connectAttr("time1.outTime", grid_node + ".currentTime") # Set path self._set_path(grid_node, path, show_preset_popup=True) # Lock the shape node so the user can't delete the transform/shape # as if it was referenced cmds.lockNode(grid_node, lock=True) nodes = [root, grid_node] self[:] = nodes return containerise( name=name, namespace=namespace, nodes=nodes, context=context, loader=self.__class__.__name__) def _set_path(self, grid_node, path, show_preset_popup=True): from openpype.hosts.maya.api.lib import attribute_values from maya import cmds def _get_filename_from_folder(path): # Using the sequence of .vdb files we check the frame range, etc. # to set the filename with #### padding. files = sorted(x for x in os.listdir(path) if x.endswith(".vdb")) if not files: raise RuntimeError("Couldn't find .vdb files in: %s" % path) if len(files) == 1: # Ensure check for single file is also done in folder fname = files[0] else: # Sequence import clique # todo: check support for negative frames as input collections, remainder = clique.assemble(files) assert len(collections) == 1, ( "Must find a single image sequence, " "found: %s" % (collections,) ) collection = collections[0] fname = collection.format('{head}{{padding}}{tail}') padding = collection.padding if padding == 0: # Clique doesn't provide padding if the frame number never # starts with a zero and thus has never any visual padding. # So we fall back to the smallest frame number as padding. padding = min(len(str(i)) for i in collection.indexes) # Supply frame/padding with # signs padding_str = "#" * padding fname = fname.format(padding=padding_str) return os.path.join(path, fname) # The path is either a single file or sequence in a folder so # we do a quick lookup for our files if os.path.isfile(path): path = os.path.dirname(path) path = _get_filename_from_folder(path) # Even when not applying a preset V-Ray will reset the 3rd Party # Channels Mapping of the VRayVolumeGrid when setting the .inPath # value. As such we try and preserve the values ourselves. # Reported as ChaosGroup bug ticket: 154-011-2909 # todo(roy): Remove when new V-Ray release preserves values original_user_mapping = cmds.getAttr(grid_node + ".usrchmap") or "" # Workaround for V-Ray bug: fix lag on path change, see function _fix_duplicate_vvg_callbacks() # Suppress preset pop-up if we want. popup_attr = "{0}.inDontOfferPresets".format(grid_node) popup = {popup_attr: not show_preset_popup} with attribute_values(popup): cmds.setAttr(grid_node + ".inPath", path, type="string") # Reapply the 3rd Party channels user mapping when no preset popup # was shown to the user if not show_preset_popup: channels = cmds.getAttr(grid_node + ".usrchmapallch").split(";") channels = set(channels) # optimize lookup restored_mapping = "" for entry in original_user_mapping.split(";"): if not entry: # Ignore empty entries continue # If 3rd Party Channels selection channel still exists then # add it again. index, channel = entry.split(",") attr = THIRD_PARTY_CHANNELS.get(int(index), # Fallback for when a mapping # was set that is not in the # documentation "???") if channel in channels: restored_mapping += entry + ";" else: self.log.warning("Can't preserve '%s' mapping due to " "missing channel '%s' on node: " "%s" % (attr, channel, grid_node)) if restored_mapping: cmds.setAttr(grid_node + ".usrchmap", restored_mapping, type="string") def update(self, container, representation): path = get_representation_path(representation) # Find VRayVolumeGrid members = cmds.sets(container['objectName'], query=True) grid_nodes = cmds.ls(members, type="VRayVolumeGrid", long=True) assert len(grid_nodes) > 0, "This is a bug" # Update the VRayVolumeGrid for grid_node in grid_nodes: self._set_path(grid_node, path=path, show_preset_popup=False) # Update container representation cmds.setAttr(container["objectName"] + ".representation", str(representation["_id"]), type="string") def switch(self, container, representation): self.update(container, representation) def remove(self, container): # Get all members of the avalon container, ensure they are unlocked # and delete everything members = cmds.sets(container['objectName'], query=True) cmds.lockNode(members, lock=False) cmds.delete([container['objectName']] + members) # Clean up the namespace try: cmds.namespace(removeNamespace=container['namespace'], deleteNamespaceContent=True) except RuntimeError: pass ================================================ FILE: openpype/hosts/maya/plugins/load/load_vrayproxy.py ================================================ # -*- coding: utf-8 -*- """Loader for Vray Proxy files. If there are Alembics published along vray proxy (in the same version), loader will use them instead of native vray vrmesh format. """ import os import maya.cmds as cmds from openpype.client import get_representation_by_name from openpype.settings import get_project_settings from openpype.pipeline import ( load, get_current_project_name, get_representation_path, ) from openpype.hosts.maya.api.lib import ( maintained_selection, namespaced, unique_namespace ) from openpype.hosts.maya.api.pipeline import containerise class VRayProxyLoader(load.LoaderPlugin): """Load VRay Proxy with Alembic or VrayMesh.""" families = ["vrayproxy", "model", "pointcache", "animation"] representations = ["vrmesh", "abc"] label = "Import VRay Proxy" order = -10 icon = "code-fork" color = "orange" def load(self, context, name=None, namespace=None, options=None): # type: (dict, str, str, dict) -> None """Loader entry point. Args: context (dict): Loaded representation context. name (str): Name of container. namespace (str): Optional namespace name. options (dict): Optional loader options. """ try: family = context["representation"]["context"]["family"] except ValueError: family = "vrayproxy" # get all representations for this version filename = self._get_abc(context["version"]["_id"]) if not filename: filename = self.filepath_from_context(context) asset_name = context['asset']["name"] namespace = namespace or unique_namespace( asset_name + "_", prefix="_" if asset_name[0].isdigit() else "", suffix="_", ) # Ensure V-Ray for Maya is loaded. cmds.loadPlugin("vrayformaya", quiet=True) with maintained_selection(): cmds.namespace(addNamespace=namespace) with namespaced(namespace, new=False): nodes, group_node = self.create_vray_proxy( name, filename=filename) self[:] = nodes if not nodes: return # colour the group node project_name = context["project"]["name"] settings = get_project_settings(project_name) colors = settings['maya']['load']['colors'] c = colors.get(family) if c is not None: cmds.setAttr("{0}.useOutlinerColor".format(group_node), 1) cmds.setAttr( "{0}.outlinerColor".format(group_node), (float(c[0]) / 255), (float(c[1]) / 255), (float(c[2]) / 255) ) return containerise( name=name, namespace=namespace, nodes=nodes, context=context, loader=self.__class__.__name__) def update(self, container, representation): # type: (dict, dict) -> None """Update container with specified representation.""" node = container['objectName'] assert cmds.objExists(node), "Missing container" members = cmds.sets(node, query=True) or [] vraymeshes = cmds.ls(members, type="VRayProxy") assert vraymeshes, "Cannot find VRayMesh in container" # get all representations for this version filename = ( self._get_abc(representation["parent"]) or get_representation_path(representation) ) for vray_mesh in vraymeshes: cmds.setAttr("{}.fileName".format(vray_mesh), filename, type="string") # Update metadata cmds.setAttr("{}.representation".format(node), str(representation["_id"]), type="string") def remove(self, container): # type: (dict) -> None """Remove loaded container.""" # Delete container and its contents if cmds.objExists(container['objectName']): members = cmds.sets(container['objectName'], query=True) or [] cmds.delete([container['objectName']] + members) # Remove the namespace, if empty namespace = container['namespace'] if cmds.namespace(exists=namespace): members = cmds.namespaceInfo(namespace, listNamespace=True) if not members: cmds.namespace(removeNamespace=namespace) else: self.log.warning("Namespace not deleted because it " "still has members: %s", namespace) def switch(self, container, representation): # type: (dict, dict) -> None """Switch loaded representation.""" self.update(container, representation) def create_vray_proxy(self, name, filename): # type: (str, str) -> (list, str) """Re-create the structure created by VRay to support vrmeshes Args: name (str): Name of the asset. filename (str): File name of vrmesh. Returns: nodes(list) """ if name is None: name = os.path.splitext(os.path.basename(filename))[0] parent = cmds.createNode("transform", name=name) proxy = cmds.createNode( "VRayProxy", name="{}Shape".format(name), parent=parent) cmds.setAttr(proxy + ".fileName", filename, type="string") cmds.connectAttr("time1.outTime", proxy + ".currentFrame") return [parent, proxy], parent def _get_abc(self, version_id): # type: (str) -> str """Get abc representation file path if present. If here is published Alembic (abc) representation published along vray proxy, get is file path. Args: version_id (str): Version hash id. Returns: str: Path to file. None: If abc not found. """ self.log.debug( "Looking for abc in published representations of this version.") project_name = get_current_project_name() abc_rep = get_representation_by_name(project_name, "abc", version_id) if abc_rep: self.log.debug("Found, we'll link alembic to vray proxy.") file_name = get_representation_path(abc_rep) self.log.debug("File: {}".format(file_name)) return file_name return "" ================================================ FILE: openpype/hosts/maya/plugins/load/load_vrayscene.py ================================================ # -*- coding: utf-8 -*- import os import maya.cmds as cmds # noqa from openpype.settings import get_project_settings from openpype.pipeline import ( load, get_representation_path ) from openpype.hosts.maya.api.lib import ( maintained_selection, namespaced, unique_namespace ) from openpype.hosts.maya.api.pipeline import containerise class VRaySceneLoader(load.LoaderPlugin): """Load Vray scene""" families = ["vrayscene_layer"] representations = ["vrscene"] label = "Import VRay Scene" order = -10 icon = "code-fork" color = "orange" def load(self, context, name, namespace, data): try: family = context["representation"]["context"]["family"] except ValueError: family = "vrayscene_layer" asset_name = context['asset']["name"] namespace = namespace or unique_namespace( asset_name + "_", prefix="_" if asset_name[0].isdigit() else "", suffix="_", ) # Ensure V-Ray for Maya is loaded. cmds.loadPlugin("vrayformaya", quiet=True) with maintained_selection(): cmds.namespace(addNamespace=namespace) with namespaced(namespace, new=False): nodes, root_node = self.create_vray_scene( name, filename=self.filepath_from_context(context) ) self[:] = nodes if not nodes: return # colour the group node project_name = context["project"]["name"] settings = get_project_settings(project_name) colors = settings['maya']['load']['colors'] c = colors.get(family) if c is not None: cmds.setAttr("{0}.useOutlinerColor".format(root_node), 1) cmds.setAttr("{0}.outlinerColor".format(root_node), (float(c[0])/255), (float(c[1])/255), (float(c[2])/255) ) return containerise( name=name, namespace=namespace, nodes=nodes, context=context, loader=self.__class__.__name__) def update(self, container, representation): node = container['objectName'] assert cmds.objExists(node), "Missing container" members = cmds.sets(node, query=True) or [] vraymeshes = cmds.ls(members, type="VRayScene") assert vraymeshes, "Cannot find VRayScene in container" filename = get_representation_path(representation) for vray_mesh in vraymeshes: cmds.setAttr("{}.FilePath".format(vray_mesh), filename, type="string") # Update metadata cmds.setAttr("{}.representation".format(node), str(representation["_id"]), type="string") def remove(self, container): # Delete container and its contents if cmds.objExists(container['objectName']): members = cmds.sets(container['objectName'], query=True) or [] cmds.delete([container['objectName']] + members) # Remove the namespace, if empty namespace = container['namespace'] if cmds.namespace(exists=namespace): members = cmds.namespaceInfo(namespace, listNamespace=True) if not members: cmds.namespace(removeNamespace=namespace) else: self.log.warning("Namespace not deleted because it " "still has members: %s", namespace) def switch(self, container, representation): self.update(container, representation) def create_vray_scene(self, name, filename): """Re-create the structure created by VRay to support vrscenes Args: name(str): name of the asset Returns: nodes(list) """ # Create nodes mesh_node_name = "VRayScene_{}".format(name) trans = cmds.createNode( "transform", name=mesh_node_name) vray_scene = cmds.createNode( "VRayScene", name="{}_VRSCN".format(mesh_node_name), parent=trans) mesh = cmds.createNode( "mesh", name="{}_Shape".format(mesh_node_name), parent=trans) cmds.connectAttr( "{}.outMesh".format(vray_scene), "{}.inMesh".format(mesh)) cmds.setAttr("{}.FilePath".format(vray_scene), filename, type="string") # Lock the shape nodes so the user cannot delete these cmds.lockNode(mesh, lock=True) cmds.lockNode(vray_scene, lock=True) # Create important connections cmds.connectAttr("time1.outTime", "{0}.inputTime".format(trans)) # Connect mesh to initialShadingGroup cmds.sets([mesh], forceElement="initialShadingGroup") nodes = [trans, vray_scene, mesh] # Fix: Force refresh so the mesh shows correctly after creation cmds.refresh() return nodes, trans ================================================ FILE: openpype/hosts/maya/plugins/load/load_xgen.py ================================================ import os import shutil import maya.cmds as cmds import xgenm from qtpy import QtWidgets import openpype.hosts.maya.api.plugin from openpype.hosts.maya.api.lib import ( maintained_selection, get_container_members, attribute_values, write_xgen_file ) from openpype.hosts.maya.api import current_file from openpype.pipeline import get_representation_path class XgenLoader(openpype.hosts.maya.api.plugin.ReferenceLoader): """Load Xgen as reference""" families = ["xgen"] representations = ["ma", "mb"] label = "Reference Xgen" icon = "code-fork" color = "orange" def get_xgen_xgd_paths(self, palette): _, maya_extension = os.path.splitext(current_file()) xgen_file = current_file().replace( maya_extension, "__{}.xgen".format(palette.replace("|", "").replace(":", "__")) ) xgd_file = xgen_file.replace(".xgen", ".xgd") return xgen_file, xgd_file def process_reference(self, context, name, namespace, options): # Validate workfile has a path. if current_file() is None: QtWidgets.QMessageBox.warning( None, "", "Current workfile has not been saved. Please save the workfile" " before loading an Xgen." ) return maya_filepath = self.prepare_root_value( file_url=self.filepath_from_context(context), project_name=context["project"]["name"] ) # Reference xgen. Xgen does not like being referenced in under a group. with maintained_selection(): nodes = cmds.file( maya_filepath, namespace=namespace, sharedReferenceFile=False, reference=True, returnNewNodes=True ) xgen_palette = cmds.ls( nodes, type="xgmPalette", long=True )[0].replace("|", "") xgen_file, xgd_file = self.get_xgen_xgd_paths(xgen_palette) self.set_palette_attributes(xgen_palette, xgen_file, xgd_file) # Change the cache and disk values of xgDataPath and xgProjectPath # to ensure paths are setup correctly. project_path = os.path.dirname(current_file()).replace("\\", "/") xgenm.setAttr("xgProjectPath", project_path, xgen_palette) data_path = "${{PROJECT}}xgen/collections/{};{}".format( xgen_palette.replace(":", "__ns__"), xgenm.getAttr("xgDataPath", xgen_palette) ) xgenm.setAttr("xgDataPath", data_path, xgen_palette) data = {"xgProjectPath": project_path, "xgDataPath": data_path} write_xgen_file(data, xgen_file) # This create an expression attribute of float. If we did not add # any changes to collection, then Xgen does not create an xgd file # on save. This gives errors when launching the workfile again due # to trying to find the xgd file. name = "custom_float_ignore" if name not in xgenm.customAttrs(xgen_palette): xgenm.addCustomAttr( "custom_float_ignore", xgen_palette ) shapes = cmds.ls(nodes, shapes=True, long=True) new_nodes = (list(set(nodes) - set(shapes))) self[:] = new_nodes return new_nodes def set_palette_attributes(self, xgen_palette, xgen_file, xgd_file): cmds.setAttr( "{}.xgBaseFile".format(xgen_palette), os.path.basename(xgen_file), type="string" ) cmds.setAttr( "{}.xgFileName".format(xgen_palette), os.path.basename(xgd_file), type="string" ) cmds.setAttr("{}.xgExportAsDelta".format(xgen_palette), True) def update(self, container, representation): """Workflow for updating Xgen. - Export changes to delta file. - Copy and overwrite the workspace .xgen file. - Set collection attributes to not include delta files. - Update xgen maya file reference. - Apply the delta file changes. - Reset collection attributes to include delta files. We have to do this workflow because when using referencing of the xgen collection, Maya implicitly imports the Xgen data from the xgen file so we dont have any control over when adding the delta file changes. There is an implicit increment of the xgen and delta files, due to using the workfile basename. """ # Storing current description to try and maintain later. current_description = ( xgenm.xgGlobal.DescriptionEditor.currentDescription() ) container_node = container["objectName"] members = get_container_members(container_node) xgen_palette = cmds.ls( members, type="xgmPalette", long=True )[0].replace("|", "") xgen_file, xgd_file = self.get_xgen_xgd_paths(xgen_palette) # Export current changes to apply later. xgenm.createDelta(xgen_palette.replace("|", ""), xgd_file) self.set_palette_attributes(xgen_palette, xgen_file, xgd_file) maya_file = get_representation_path(representation) _, extension = os.path.splitext(maya_file) new_xgen_file = maya_file.replace(extension, ".xgen") data_path = "" with open(new_xgen_file, "r") as f: for line in f: if line.startswith("\txgDataPath"): line = line.rstrip() data_path = line.split("\t")[-1] break project_path = os.path.dirname(current_file()).replace("\\", "/") data_path = "${{PROJECT}}xgen/collections/{};{}".format( xgen_palette.replace(":", "__ns__"), data_path ) data = {"xgProjectPath": project_path, "xgDataPath": data_path} shutil.copy(new_xgen_file, xgen_file) write_xgen_file(data, xgen_file) attribute_data = { "{}.xgFileName".format(xgen_palette): os.path.basename(xgen_file), "{}.xgBaseFile".format(xgen_palette): "", "{}.xgExportAsDelta".format(xgen_palette): False } with attribute_values(attribute_data): super().update(container, representation) xgenm.applyDelta(xgen_palette.replace("|", ""), xgd_file) # Restore current selected description if it exists. if cmds.objExists(current_description): xgenm.xgGlobal.DescriptionEditor.setCurrentDescription( current_description ) # Full UI refresh. xgenm.xgGlobal.DescriptionEditor.refresh("Full") ================================================ FILE: openpype/hosts/maya/plugins/load/load_yeti_cache.py ================================================ import os import json import re from collections import defaultdict import clique from maya import cmds from openpype.settings import get_project_settings from openpype.pipeline import ( load, get_representation_path ) from openpype.hosts.maya.api import lib from openpype.hosts.maya.api.pipeline import containerise # Do not reset these values on update but only apply on first load # to preserve any potential local overrides SKIP_UPDATE_ATTRS = { "displayOutput", "viewportDensity", "viewportWidth", "viewportLength", } def set_attribute(node, attr, value): """Wrapper of set attribute which ignores None values""" if value is None: return lib.set_attribute(node, attr, value) class YetiCacheLoader(load.LoaderPlugin): """Load Yeti Cache with one or more Yeti nodes""" families = ["yeticache", "yetiRig"] representations = ["fur"] label = "Load Yeti Cache" order = -9 icon = "code-fork" color = "orange" def load(self, context, name=None, namespace=None, data=None): """Loads a .fursettings file defining how to load .fur sequences A single yeticache or yetiRig can have more than a single pgYetiMaya nodes and thus load more than a single yeti.fur sequence. The .fursettings file defines what the node names should be and also what "cbId" attribute they should receive to match the original source and allow published looks to also work for Yeti rigs and its caches. """ try: family = context["representation"]["context"]["family"] except ValueError: family = "yeticache" # Build namespace asset = context["asset"] if namespace is None: namespace = self.create_namespace(asset["name"]) # Ensure Yeti is loaded if not cmds.pluginInfo("pgYetiMaya", query=True, loaded=True): cmds.loadPlugin("pgYetiMaya", quiet=True) # Create Yeti cache nodes according to settings path = self.filepath_from_context(context) settings = self.read_settings(path) nodes = [] for node in settings["nodes"]: nodes.extend(self.create_node(namespace, node)) group_name = "{}:{}".format(namespace, name) group_node = cmds.group(nodes, name=group_name) project_name = context["project"]["name"] settings = get_project_settings(project_name) colors = settings['maya']['load']['colors'] c = colors.get(family) if c is not None: cmds.setAttr(group_node + ".useOutlinerColor", 1) cmds.setAttr(group_node + ".outlinerColor", (float(c[0])/255), (float(c[1])/255), (float(c[2])/255) ) nodes.append(group_node) self[:] = nodes return containerise( name=name, namespace=namespace, nodes=nodes, context=context, loader=self.__class__.__name__ ) def remove(self, container): from maya import cmds namespace = container["namespace"] container_name = container["objectName"] self.log.info("Removing '%s' from Maya.." % container["name"]) container_content = cmds.sets(container_name, query=True) nodes = cmds.ls(container_content, long=True) nodes.append(container_name) try: cmds.delete(nodes) except ValueError: # Already implicitly deleted by Maya upon removing reference pass cmds.namespace(removeNamespace=namespace, deleteNamespaceContent=True) def update(self, container, representation): namespace = container["namespace"] container_node = container["objectName"] path = get_representation_path(representation) settings = self.read_settings(path) # Collect scene information of asset set_members = lib.get_container_members(container) container_root = lib.get_container_transforms(container, members=set_members, root=True) scene_nodes = cmds.ls(set_members, type="pgYetiMaya", long=True) # Build lookup with cbId as keys scene_lookup = defaultdict(list) for node in scene_nodes: cb_id = lib.get_id(node) scene_lookup[cb_id].append(node) # Re-assemble metadata with cbId as keys meta_data_lookup = {n["cbId"]: n for n in settings["nodes"]} # Delete nodes by "cbId" that are not in the updated version to_delete_lookup = {cb_id for cb_id in scene_lookup.keys() if cb_id not in meta_data_lookup} if to_delete_lookup: # Get nodes and remove entry from lookup to_remove = [] for _id in to_delete_lookup: # Get all related nodes shapes = scene_lookup[_id] # Get the parents of all shapes under the ID transforms = cmds.listRelatives(shapes, parent=True, fullPath=True) or [] to_remove.extend(shapes + transforms) # Remove id from lookup scene_lookup.pop(_id, None) cmds.delete(to_remove) for cb_id, node_settings in meta_data_lookup.items(): if cb_id not in scene_lookup: # Create new nodes self.log.info("Creating new nodes ..") new_nodes = self.create_node(namespace, node_settings) cmds.sets(new_nodes, addElement=container_node) cmds.parent(new_nodes, container_root) else: # Update the matching nodes scene_nodes = scene_lookup[cb_id] lookup_result = meta_data_lookup[cb_id]["name"] # Remove namespace if any (e.g.: "character_01_:head_YNShape") node_name = lookup_result.rsplit(":", 1)[-1] for scene_node in scene_nodes: # Get transform node, this makes renaming easier transforms = cmds.listRelatives(scene_node, parent=True, fullPath=True) or [] assert len(transforms) == 1, "This is a bug!" # Get scene node's namespace and rename the transform node lead = scene_node.rsplit(":", 1)[0] namespace = ":{}".format(lead.rsplit("|")[-1]) new_shape_name = "{}:{}".format(namespace, node_name) new_trans_name = new_shape_name.rsplit("Shape", 1)[0] transform_node = transforms[0] cmds.rename(transform_node, new_trans_name, ignoreShape=False) # Get the newly named shape node yeti_nodes = cmds.listRelatives(new_trans_name, children=True) yeti_node = yeti_nodes[0] for attr, value in node_settings["attrs"].items(): if attr in SKIP_UPDATE_ATTRS: continue set_attribute(attr, value, yeti_node) cmds.setAttr("{}.representation".format(container_node), str(representation["_id"]), typ="string") def switch(self, container, representation): self.update(container, representation) # helper functions def create_namespace(self, asset): """Create a unique namespace Args: asset (dict): asset information """ asset_name = "{}_".format(asset) prefix = "_" if asset_name[0].isdigit()else "" namespace = lib.unique_namespace( asset_name, prefix=prefix, suffix="_" ) return namespace def get_cache_node_filepath(self, root, node_name): """Get the cache file path for one of the yeti nodes. All caches with more than 1 frame need cache file name set with `%04d` If the cache has only one frame we return the file name as we assume it is a snapshot. This expects the files to be named after the "node name" through exports with in Yeti. Args: root(str): Folder containing cache files to search in. node_name(str): Node name to search cache files for Returns: str: Cache file path value needed for cacheFileName attribute """ name = node_name.replace(":", "_") pattern = r"^({name})(\.[0-9]+)?(\.fur)$".format(name=re.escape(name)) files = [fname for fname in os.listdir(root) if re.match(pattern, fname)] if not files: self.log.error("Could not find cache files for '{}' " "with pattern {}".format(node_name, pattern)) return if len(files) == 1: # Single file return os.path.join(root, files[0]) # Get filename for the sequence with padding collections, remainder = clique.assemble(files) assert not remainder, "This is a bug" assert len(collections) == 1, "This is a bug" collection = collections[0] # Formats name as {head}%d{tail} like cache.%04d.fur fname = collection.format("{head}{padding}{tail}") return os.path.join(root, fname) def create_node(self, namespace, node_settings): """Create nodes with the correct namespace and settings Args: namespace(str): namespace node_settings(dict): Single "nodes" entry from .fursettings file. Returns: list: Created nodes """ nodes = [] # Get original names and ids orig_transform_name = node_settings["transform"]["name"] orig_shape_name = node_settings["name"] # Add namespace transform_name = "{}:{}".format(namespace, orig_transform_name) shape_name = "{}:{}".format(namespace, orig_shape_name) # Create pgYetiMaya node transform_node = cmds.createNode("transform", name=transform_name) yeti_node = cmds.createNode("pgYetiMaya", name=shape_name, parent=transform_node) lib.set_id(transform_node, node_settings["transform"]["cbId"]) lib.set_id(yeti_node, node_settings["cbId"]) nodes.extend([transform_node, yeti_node]) # Update attributes with defaults attributes = node_settings["attrs"] attributes.update({ "verbosity": 2, "fileMode": 1, # Fix render stats, like Yeti's own # ../scripts/pgYetiNode.mel script "visibleInReflections": True, "visibleInRefractions": True }) if "viewportDensity" not in attributes: attributes["viewportDensity"] = 0.1 # Apply attributes to pgYetiMaya node for attr, value in attributes.items(): set_attribute(attr, value, yeti_node) # Connect to the time node cmds.connectAttr("time1.outTime", "%s.currentTime" % yeti_node) return nodes def read_settings(self, path): """Read .fursettings file and compute some additional attributes""" with open(path, "r") as fp: fur_settings = json.load(fp) if "nodes" not in fur_settings: raise RuntimeError("Encountered invalid data, " "expected 'nodes' in fursettings.") # Compute the cache file name values we want to set for the nodes root = os.path.dirname(path) for node in fur_settings["nodes"]: cache_filename = self.get_cache_node_filepath( root=root, node_name=node["name"]) attrs = node.get("attrs", {}) # allow 'attrs' to not exist attrs["cacheFileName"] = cache_filename node["attrs"] = attrs return fur_settings ================================================ FILE: openpype/hosts/maya/plugins/load/load_yeti_rig.py ================================================ import maya.cmds as cmds from openpype.settings import get_current_project_settings import openpype.hosts.maya.api.plugin from openpype.hosts.maya.api import lib class YetiRigLoader(openpype.hosts.maya.api.plugin.ReferenceLoader): """This loader will load Yeti rig.""" families = ["yetiRig"] representations = ["ma"] label = "Load Yeti Rig" order = -9 icon = "code-fork" color = "orange" def process_reference( self, context, name=None, namespace=None, options=None ): path = self.filepath_from_context(context) attach_to_root = options.get("attach_to_root", True) group_name = options["group_name"] # no group shall be created if not attach_to_root: group_name = namespace with lib.maintained_selection(): file_url = self.prepare_root_value( path, context["project"]["name"] ) nodes = cmds.file( file_url, namespace=namespace, reference=True, returnNewNodes=True, groupReference=attach_to_root, groupName=group_name ) settings = get_current_project_settings() colors = settings["maya"]["load"]["colors"] c = colors.get("yetiRig") if c is not None: cmds.setAttr(group_name + ".useOutlinerColor", 1) cmds.setAttr( group_name + ".outlinerColor", (float(c[0]) / 255), (float(c[1]) / 255), (float(c[2]) / 255) ) self[:] = nodes return nodes ================================================ FILE: openpype/hosts/maya/plugins/publish/__init__.py ================================================ ================================================ FILE: openpype/hosts/maya/plugins/publish/collect_animation.py ================================================ import pyblish.api import maya.cmds as cmds class CollectAnimationOutputGeometry(pyblish.api.InstancePlugin): """Collect out hierarchy data for instance. Collect all hierarchy nodes which reside in the out_SET of the animation instance or point cache instance. This is to unify the logic of retrieving that specific data. This eliminates the need to write two separate pieces of logic to fetch all hierarchy nodes. Results in a list of nodes from the content of the instances """ order = pyblish.api.CollectorOrder + 0.4 families = ["animation"] label = "Collect Animation" hosts = ["maya"] ignore_type = ["constraints"] def process(self, instance): """Collect the hierarchy nodes""" family = instance.data["family"] out_set = next((i for i in instance.data["setMembers"] if i.endswith("out_SET")), None) if out_set is None: warning = "Expecting out_SET for instance of family '%s'" % family self.log.warning(warning) return members = cmds.ls(cmds.sets(out_set, query=True), long=True) # Get all the relatives of the members descendants = cmds.listRelatives(members, allDescendents=True, fullPath=True) or [] descendants = cmds.ls(descendants, noIntermediate=True, long=True) # Add members and descendants together for a complete overview hierarchy = members + descendants # Ignore certain node types (e.g. constraints) ignore = cmds.ls(hierarchy, type=self.ignore_type, long=True) if ignore: ignore = set(ignore) hierarchy = [node for node in hierarchy if node not in ignore] # Store data in the instance for the validator instance.data["out_hierarchy"] = hierarchy if instance.data.get("farm"): instance.data["families"].append("publish.farm") # User defined attributes. instance.data["includeUserDefinedAttributes"] = ( instance.data["creator_attributes"]["includeUserDefinedAttributes"] ) ================================================ FILE: openpype/hosts/maya/plugins/publish/collect_arnold_scene_source.py ================================================ from maya import cmds import pyblish.api from openpype.hosts.maya.api.lib import get_all_children class CollectArnoldSceneSource(pyblish.api.InstancePlugin): """Collect Arnold Scene Source data.""" # Offset to be after renderable camera collection. order = pyblish.api.CollectorOrder + 0.2 label = "Collect Arnold Scene Source" families = ["ass", "assProxy"] def process(self, instance): instance.data["members"] = [] for set_member in instance.data["setMembers"]: if cmds.nodeType(set_member) != "objectSet": instance.data["members"].extend(self.get_hierarchy(set_member)) continue members = cmds.sets(set_member, query=True) members = cmds.ls(members, long=True) if members is None: self.log.warning( "Skipped empty instance: \"%s\" " % set_member ) continue if set_member.endswith("proxy_SET"): instance.data["proxy"] = self.get_hierarchy(members) # Use camera in object set if present else default to render globals # camera. cameras = cmds.ls(type="camera", long=True) renderable = [c for c in cameras if cmds.getAttr("%s.renderable" % c)] if renderable: camera = renderable[0] for node in instance.data["members"]: camera_shapes = cmds.listRelatives( node, shapes=True, type="camera" ) if camera_shapes: camera = node instance.data["camera"] = camera else: self.log.debug("No renderable cameras found.") self.log.debug("data: {}".format(instance.data)) def get_hierarchy(self, nodes): """Return nodes with all their children""" nodes = cmds.ls(nodes, long=True) if not nodes: return [] children = get_all_children(nodes) # Make sure nodes merged with children only # contains unique entries return list(set(nodes + children)) ================================================ FILE: openpype/hosts/maya/plugins/publish/collect_assembly.py ================================================ from collections import defaultdict import pyblish.api from maya import cmds, mel from openpype.hosts.maya import api from openpype.hosts.maya.api import lib # TODO : Publish of assembly: -unique namespace for all assets, VALIDATOR! class CollectAssembly(pyblish.api.InstancePlugin): """Collect all relevant assembly items Collected data: * File name * Compatible loader * Matrix per instance * Namespace Note: GPU caches are currently not supported in the pipeline. There is no logic yet which supports the swapping of GPU cache to renderable objects. """ order = pyblish.api.CollectorOrder + 0.49 label = "Assembly" families = ["assembly"] def process(self, instance): # Find containers containers = api.ls() # Get all content from the instance instance_lookup = set(cmds.ls(instance, type="transform", long=True)) data = defaultdict(list) hierarchy_nodes = [] for container in containers: root = lib.get_container_transforms(container, root=True) if not root or root not in instance_lookup: continue # Retrieve the hierarchy parent = cmds.listRelatives(root, parent=True, fullPath=True)[0] hierarchy_nodes.append(parent) # Temporary warning for GPU cache which are not supported yet loader = container["loader"] if loader == "GpuCacheLoader": self.log.warning("GPU Cache Loader is currently not supported" "in the pipeline, we will export it tho") # Gather info for new data entry representation_id = container["representation"] instance_data = {"loader": loader, "parent": parent, "namespace": container["namespace"]} # Check if matrix differs from default and store changes matrix_data = self.get_matrix_data(root) if matrix_data: instance_data["matrix"] = matrix_data data[representation_id].append(instance_data) instance.data["scenedata"] = dict(data) instance.data["nodesHierarchy"] = list(set(hierarchy_nodes)) def get_file_rule(self, rule): return mel.eval('workspace -query -fileRuleEntry "{}"'.format(rule)) def get_matrix_data(self, node): """Get the matrix of all members when they are not default Each matrix which differs from the default will be stored in a dictionary Args: members (list): list of transform nmodes Returns: dict """ matrix = cmds.xform(node, query=True, matrix=True) if matrix == lib.DEFAULT_MATRIX: return return matrix ================================================ FILE: openpype/hosts/maya/plugins/publish/collect_current_file.py ================================================ import pyblish.api from maya import cmds class CollectCurrentFile(pyblish.api.ContextPlugin): """Inject the current working file.""" order = pyblish.api.CollectorOrder - 0.4 label = "Maya Current File" hosts = ['maya'] def process(self, context): """Inject the current working file""" context.data['currentFile'] = cmds.file(query=True, sceneName=True) ================================================ FILE: openpype/hosts/maya/plugins/publish/collect_fbx_animation.py ================================================ # -*- coding: utf-8 -*- from maya import cmds # noqa import pyblish.api from openpype.pipeline import OptionalPyblishPluginMixin class CollectFbxAnimation(pyblish.api.InstancePlugin, OptionalPyblishPluginMixin): """Collect Animated Rig Data for FBX Extractor.""" order = pyblish.api.CollectorOrder + 0.2 label = "Collect Fbx Animation" hosts = ["maya"] families = ["animation"] optional = True def process(self, instance): if not self.is_active(instance.data): return skeleton_sets = [ i for i in instance if i.endswith("skeletonAnim_SET") ] if not skeleton_sets: return instance.data["families"].append("animation.fbx") instance.data["animated_skeleton"] = [] for skeleton_set in skeleton_sets: skeleton_content = cmds.sets(skeleton_set, query=True) self.log.debug( "Collected animated skeleton data: {}".format( skeleton_content )) if skeleton_content: instance.data["animated_skeleton"] = skeleton_content ================================================ FILE: openpype/hosts/maya/plugins/publish/collect_fbx_camera.py ================================================ # -*- coding: utf-8 -*- from maya import cmds # noqa import pyblish.api class CollectFbxCamera(pyblish.api.InstancePlugin): """Collect Camera for FBX export.""" order = pyblish.api.CollectorOrder + 0.2 label = "Collect Camera for FBX export" families = ["camera"] def process(self, instance): if not instance.data.get("families"): instance.data["families"] = [] if "fbx" not in instance.data["families"]: instance.data["families"].append("fbx") instance.data["cameras"] = True ================================================ FILE: openpype/hosts/maya/plugins/publish/collect_file_dependencies.py ================================================ import json from maya import cmds import pyblish.api class CollectFileDependencies(pyblish.api.ContextPlugin): """Gather all files referenced in this scene.""" label = "Collect File Dependencies" order = pyblish.api.CollectorOrder - 0.49 hosts = ["maya"] def process(self, context): dependencies = [] for node in cmds.ls(type="file"): path = cmds.getAttr("{}.{}".format(node, "fileTextureName")) if path not in dependencies: dependencies.append(path) for node in cmds.ls(type="AlembicNode"): path = cmds.getAttr("{}.{}".format(node, "abc_File")) if path not in dependencies: dependencies.append(path) context.data["fileDependencies"] = dependencies self.log.debug(json.dumps(dependencies, indent=4)) ================================================ FILE: openpype/hosts/maya/plugins/publish/collect_gltf.py ================================================ # -*- coding: utf-8 -*- import pyblish.api class CollectGLTF(pyblish.api.InstancePlugin): """Collect Assets for GLTF/GLB export.""" order = pyblish.api.CollectorOrder + 0.2 label = "Collect Asset for GLTF/GLB export" families = ["model", "animation", "pointcache"] def process(self, instance): if not instance.data.get("families"): instance.data["families"] = [] if "gltf" not in instance.data["families"]: instance.data["families"].append("gltf") ================================================ FILE: openpype/hosts/maya/plugins/publish/collect_history.py ================================================ from maya import cmds import pyblish.api class CollectMayaHistory(pyblish.api.InstancePlugin): """Collect history for instances from the Maya scene Note: This removes render layers collected in the history This is separate from Collect Instances so we can target it towards only specific family types. """ order = pyblish.api.CollectorOrder + 0.1 hosts = ["maya"] label = "Maya History" families = ["rig"] def process(self, instance): kwargs = {} if int(cmds.about(version=True)) >= 2020: # New flag since Maya 2020 which makes cmds.listHistory faster kwargs = {"fastIteration": True} else: self.log.debug("Ignoring `fastIteration` flag before Maya 2020..") # Collect the history with long names history = set(cmds.listHistory(instance, leaf=False, **kwargs) or []) history = cmds.ls(list(history), long=True) # Exclude invalid nodes (like renderlayers) exclude = cmds.ls(type="renderLayer", long=True) if exclude: exclude = set(exclude) # optimize lookup history = [x for x in history if x not in exclude] # Combine members with history members = instance[:] + history members = list(set(members)) # ensure unique # Update the instance instance[:] = members ================================================ FILE: openpype/hosts/maya/plugins/publish/collect_inputs.py ================================================ import copy from maya import cmds import maya.api.OpenMaya as om import pyblish.api from openpype.pipeline import registered_host from openpype.hosts.maya.api.lib import get_container_members from openpype.hosts.maya.api.lib_rendersetup import get_shader_in_layer def iter_history(nodes, filter=om.MFn.kInvalid, direction=om.MItDependencyGraph.kUpstream): """Iterate unique upstream history for list of nodes. This acts as a replacement to maya.cmds.listHistory. It's faster by about 2x-3x. It returns less than maya.cmds.listHistory as it excludes the input nodes from the output (unless an input node was history for another input node). It also excludes duplicates. Args: nodes (list): Maya node names to start search from. filter (om.MFn.Type): Filter to only specific types. e.g. to dag nodes using om.MFn.kDagNode direction (om.MItDependencyGraph.Direction): Direction to traverse in. Defaults to upstream. Yields: str: Node names in upstream history. """ if not nodes: return sel = om.MSelectionList() for node in nodes: sel.add(node) it = om.MItDependencyGraph(sel.getDependNode(0)) # init iterator handle = om.MObjectHandle traversed = set() fn_dep = om.MFnDependencyNode() fn_dag = om.MFnDagNode() for i in range(sel.length()): start_node = sel.getDependNode(i) start_node_hash = handle(start_node).hashCode() if start_node_hash in traversed: continue it.resetTo(start_node, filter=filter, direction=direction) while not it.isDone(): node = it.currentNode() node_hash = handle(node).hashCode() if node_hash in traversed: it.prune() it.next() # noqa: B305 continue traversed.add(node_hash) if node.hasFn(om.MFn.kDagNode): fn_dag.setObject(node) yield fn_dag.fullPathName() else: fn_dep.setObject(node) yield fn_dep.name() it.next() # noqa: B305 def collect_input_containers(containers, nodes): """Collect containers that contain any of the node in `nodes`. This will return any loaded Avalon container that contains at least one of the nodes. As such, the Avalon container is an input for it. Or in short, there are member nodes of that container. Returns: list: Input avalon containers """ # Assume the containers have collected their cached '_members' data # in the collector. return [container for container in containers if any(node in container["_members"] for node in nodes)] class CollectUpstreamInputs(pyblish.api.InstancePlugin): """Collect input source inputs for this publish. This will include `inputs` data of which loaded publishes were used in the generation of this publish. This leaves an upstream trace to what was used as input. """ label = "Collect Inputs" order = pyblish.api.CollectorOrder + 0.34 hosts = ["maya"] def process(self, instance): # For large scenes the querying of "host.ls()" can be relatively slow # e.g. up to a second. Many instances calling it easily slows this # down. As such, we cache it so we trigger it only once. # todo: Instead of hidden cache make "CollectContainers" plug-in cache_key = "__cache_containers" scene_containers = instance.context.data.get(cache_key, None) if scene_containers is None: # Query the scenes' containers if there's no cache yet host = registered_host() scene_containers = list(host.ls()) for container in scene_containers: # Embed the members into the container dictionary container_members = set(get_container_members(container)) container["_members"] = container_members instance.context.data["__cache_containers"] = scene_containers # Collect the relevant input containers for this instance if "renderlayer" in set(instance.data.get("families", [])): # Special behavior for renderlayers self.log.debug("Collecting renderlayer inputs....") containers = self._collect_renderlayer_inputs(scene_containers, instance) else: # Basic behavior nodes = instance[:] # Include any input connections of history with long names # For optimization purposes only trace upstream from shape nodes # looking for used dag nodes. This way having just a constraint # on a transform is also ignored which tended to give irrelevant # inputs for the majority of our use cases. We tend to care more # about geometry inputs. shapes = cmds.ls(nodes, type=("mesh", "nurbsSurface", "nurbsCurve"), noIntermediate=True) if shapes: history = list(iter_history(shapes, filter=om.MFn.kShape)) history = cmds.ls(history, long=True) # Include the transforms in the collected history as shapes # are excluded from containers transforms = cmds.listRelatives(cmds.ls(history, shapes=True), parent=True, fullPath=True, type="transform") if transforms: history.extend(transforms) if history: nodes = list(set(nodes + history)) # Collect containers for the given set of nodes containers = collect_input_containers(scene_containers, nodes) inputs = [c["representation"] for c in containers] instance.data["inputRepresentations"] = inputs self.log.debug("Collected inputs: %s" % inputs) def _collect_renderlayer_inputs(self, scene_containers, instance): """Collects inputs from nodes in renderlayer, incl. shaders + camera""" # Get the renderlayer renderlayer = instance.data.get("renderlayer") if renderlayer == "defaultRenderLayer": # Assume all loaded containers in the scene are inputs # for the masterlayer return copy.deepcopy(scene_containers) else: # Get the members of the layer members = cmds.editRenderLayerMembers(renderlayer, query=True, fullNames=True) or [] # In some cases invalid objects are returned from # `editRenderLayerMembers` so we filter them out members = cmds.ls(members, long=True) # Include all children children = cmds.listRelatives(members, allDescendents=True, fullPath=True) or [] members.extend(children) # Include assigned shaders in renderlayer shapes = cmds.ls(members, shapes=True, long=True) shaders = set() for shape in shapes: shape_shaders = get_shader_in_layer(shape, layer=renderlayer) if not shape_shaders: continue shaders.update(shape_shaders) members.extend(shaders) # Explicitly include the camera being rendered in renderlayer cameras = instance.data.get("cameras") members.extend(cameras) containers = collect_input_containers(scene_containers, members) return containers ================================================ FILE: openpype/hosts/maya/plugins/publish/collect_instances.py ================================================ from maya import cmds import pyblish.api from openpype.hosts.maya.api.lib import get_all_children class CollectNewInstances(pyblish.api.InstancePlugin): """Gather members for instances and pre-defined attribute This collector takes into account assets that are associated with an objectSet and marked with a unique identifier; Identifier: id (str): "pyblish.avalon.instance" Limitations: - Does not take into account nodes connected to those within an objectSet. Extractors are assumed to export with history preserved, but this limits what they will be able to achieve and the amount of data available to validators. An additional collector could also append this input data into the instance, as we do for `pype.rig` with collect_history. """ label = "Collect New Instance Data" order = pyblish.api.CollectorOrder hosts = ["maya"] valid_empty_families = {"workfile", "renderlayer"} def process(self, instance): objset = instance.data.get("instance_node") if not objset: self.log.debug("Instance has no `instance_node` data") # TODO: We might not want to do this in the future # Merge creator attributes into instance.data just backwards compatible # code still runs as expected creator_attributes = instance.data.get("creator_attributes", {}) if creator_attributes: instance.data.update(creator_attributes) members = cmds.sets(objset, query=True) or [] if members: # Collect members members = cmds.ls(members, long=True) or [] dag_members = cmds.ls(members, type="dagNode", long=True) children = get_all_children(dag_members) children = cmds.ls(children, noIntermediate=True, long=True) parents = ( self.get_all_parents(members) if creator_attributes.get("includeParentHierarchy", True) else [] ) members_hierarchy = list(set(members + children + parents)) instance[:] = members_hierarchy elif instance.data["family"] not in self.valid_empty_families: self.log.warning("Empty instance: \"%s\" " % objset) # Store the exact members of the object set instance.data["setMembers"] = members # TODO: This might make more sense as a separate collector # Convert frame values to integers for attr_name in ( "handleStart", "handleEnd", "frameStart", "frameEnd", ): value = instance.data.get(attr_name) if value is not None: instance.data[attr_name] = int(value) # Append start frame and end frame to label if present if "frameStart" in instance.data and "frameEnd" in instance.data: # Take handles from context if not set locally on the instance for key in ["handleStart", "handleEnd"]: if key not in instance.data: value = instance.context.data[key] if value is not None: value = int(value) instance.data[key] = value instance.data["frameStartHandle"] = int( instance.data["frameStart"] - instance.data["handleStart"] ) instance.data["frameEndHandle"] = int( instance.data["frameEnd"] + instance.data["handleEnd"] ) def get_all_parents(self, nodes): """Get all parents by using string operations (optimization) Args: nodes (list): the nodes which are found in the objectSet Returns: list """ parents = [] for node in nodes: splitted = node.split("|") items = ["|".join(splitted[0:i]) for i in range(2, len(splitted))] parents.extend(items) return list(set(parents)) ================================================ FILE: openpype/hosts/maya/plugins/publish/collect_look.py ================================================ # -*- coding: utf-8 -*- """Maya look collector.""" import re import os import glob from maya import cmds # noqa import pyblish.api from openpype.hosts.maya.api import lib SHAPE_ATTRS = ["castsShadows", "receiveShadows", "motionBlur", "primaryVisibility", "smoothShading", "visibleInReflections", "visibleInRefractions", "doubleSided", "opposite"] SHAPE_ATTRS = set(SHAPE_ATTRS) def get_pxr_multitexture_file_attrs(node): attrs = [] for i in range(9): if cmds.attributeQuery("filename{}".format(i), node=node, ex=True): file = cmds.getAttr("{}.filename{}".format(node, i)) if file: attrs.append("filename{}".format(i)) return attrs FILE_NODES = { # maya "file": "fileTextureName", # arnold (mtoa) "aiImage": "filename", # redshift "RedshiftNormalMap": "tex0", # renderman "PxrBump": "filename", "PxrNormalMap": "filename", "PxrMultiTexture": get_pxr_multitexture_file_attrs, "PxrPtexture": "filename", "PxrTexture": "filename" } RENDER_SET_TYPES = [ "VRayDisplacement", "VRayLightMesh", "VRayObjectProperties", "RedshiftObjectId", "RedshiftMeshParameters", ] # Keep only node types that actually exist all_node_types = set(cmds.allNodeTypes()) for node_type in list(FILE_NODES.keys()): if node_type not in all_node_types: FILE_NODES.pop(node_type) for node_type in RENDER_SET_TYPES: if node_type not in all_node_types: RENDER_SET_TYPES.remove(node_type) del all_node_types # Cache pixar dependency node types so we can perform a type lookup against it PXR_NODES = set() if cmds.pluginInfo("RenderMan_for_Maya", query=True, loaded=True): PXR_NODES = set( cmds.pluginInfo("RenderMan_for_Maya", query=True, dependNode=True) ) def get_attributes(dictionary, attr, node=None): # type: (dict, str, str) -> list if callable(dictionary[attr]): val = dictionary[attr](node) else: val = dictionary.get(attr, []) return val if isinstance(val, list) else [val] def get_look_attrs(node): """Returns attributes of a node that are important for the look. These are the "changed" attributes (those that have edits applied in the current scene). Returns: list: Attribute names to extract """ # When referenced get only attributes that are "changed since file open" # which includes any reference edits, otherwise take *all* user defined # attributes is_referenced = cmds.referenceQuery(node, isNodeReferenced=True) result = cmds.listAttr(node, userDefined=True, changedSinceFileOpen=is_referenced) or [] # `cbId` is added when a scene is saved, ignore by default if "cbId" in result: result.remove("cbId") # For shapes allow render stat changes if cmds.objectType(node, isAType="shape"): attrs = cmds.listAttr(node, changedSinceFileOpen=True) or [] for attr in attrs: if attr in SHAPE_ATTRS or \ attr not in SHAPE_ATTRS and attr.startswith('ai'): result.append(attr) return result def node_uses_image_sequence(node, node_path): # type: (str, str) -> bool """Return whether file node uses an image sequence or single image. Determine if a node uses an image sequence or just a single image, not always obvious from its file path alone. Args: node (str): Name of the Maya node node_path (str): The file path of the node Returns: bool: True if node uses an image sequence """ # useFrameExtension indicates an explicit image sequence try: use_frame_extension = cmds.getAttr('%s.useFrameExtension' % node) except ValueError: use_frame_extension = False if use_frame_extension: return True # The following tokens imply a sequence patterns = [" ", " ", " ", "u_v ", " "] node_path_lowered = node_path.lower() return any(pattern in node_path_lowered for pattern in patterns) def seq_to_glob(path): """Takes an image sequence path and returns it in glob format, with the frame number replaced by a '*'. Image sequences may be numerical sequences, e.g. /path/to/file.1001.exr will return as /path/to/file.*.exr. Image sequences may also use tokens to denote sequences, e.g. /path/to/texture. .tif will return as /path/to/texture.*.tif. Args: path (str): the image sequence path Returns: str: Return glob string that matches the filename pattern. """ if path is None: return path # If any of the patterns, convert the pattern patterns = { " ": " ", " ": " ", " ": " ", "#": "#", "u_v ": "| ", " ", " ": " " } lower = path.lower() has_pattern = False for pattern, regex_pattern in patterns.items(): if pattern in lower: path = re.sub(regex_pattern, "*", path, flags=re.IGNORECASE) has_pattern = True if has_pattern: return path base = os.path.basename(path) matches = list(re.finditer(r'\d+', base)) if matches: match = matches[-1] new_base = '{0}*{1}'.format(base[:match.start()], base[match.end():]) head = os.path.dirname(path) return os.path.join(head, new_base) else: return path def get_file_node_paths(node): # type: (str) -> list """Get the file path used by a Maya file node. Args: node (str): Name of the Maya file node Returns: list: the file paths in use """ # if the path appears to be sequence, use computedFileTextureNamePattern, # this preserves the <> tag if cmds.attributeQuery('computedFileTextureNamePattern', node=node, exists=True): plug = '{0}.computedFileTextureNamePattern'.format(node) texture_pattern = cmds.getAttr(plug) patterns = [" ", " ", "u_v ", " ", " "] lower = texture_pattern.lower() if any(pattern in lower for pattern in patterns): return [texture_pattern] try: file_attributes = get_attributes( FILE_NODES, cmds.nodeType(node), node) except AttributeError: file_attributes = "fileTextureName" files = [] for file_attr in file_attributes: if cmds.attributeQuery(file_attr, node=node, exists=True): files.append(cmds.getAttr("{}.{}".format(node, file_attr))) return files def get_file_node_files(node): """Return the file paths related to the file node Note: Will only return existing files. Returns an empty list if not valid existing files are linked. Returns: list: List of full file paths. """ paths = get_file_node_paths(node) # For sequences get all files and filter to only existing files result = [] for path in paths: if node_uses_image_sequence(node, path): glob_pattern = seq_to_glob(path) result.extend(glob.glob(glob_pattern)) elif os.path.exists(path): result.append(path) return result class CollectLook(pyblish.api.InstancePlugin): """Collect look data for instance. For the shapes/transforms of the referenced object to collect look for retrieve the user-defined attributes (like V-ray attributes) and their values as they were created in the current scene. For the members of the instance collect the sets (shadingEngines and other sets, e.g. VRayDisplacement) they are in along with the exact membership relations. Collects: lookAttributes (list): Nodes in instance with their altered attributes lookSetRelations (list): Sets and their memberships lookSets (list): List of set names included in the look """ order = pyblish.api.CollectorOrder + 0.2 families = ["look"] label = "Collect Look" hosts = ["maya"] maketx = True def process(self, instance): """Collect the Look in the instance with the correct layer settings""" renderlayer = instance.data.get("renderlayer", "defaultRenderLayer") with lib.renderlayer(renderlayer): self.collect(instance) def collect(self, instance): """Collect looks. Args: instance: Instance to collect. """ self.log.debug("Looking for look associations " "for %s" % instance.data['name']) # Lookup set (optimization) instance_lookup = set(cmds.ls(instance, long=True)) # Discover related object sets self.log.debug("Gathering sets ...") sets = self.collect_sets(instance) # Lookup set (optimization) instance_lookup = set(cmds.ls(instance, long=True)) self.log.debug("Gathering set relations ...") # Ensure iteration happen in a list to allow removing keys from the # dict within the loop for obj_set in list(sets): self.log.debug("From {}".format(obj_set)) # Get all nodes of the current objectSet (shadingEngine) for member in cmds.ls(cmds.sets(obj_set, query=True), long=True): member_data = self.collect_member_data(member, instance_lookup) if member_data: # Add information of the node to the members list sets[obj_set]["members"].append(member_data) # Remove sets that didn't have any members assigned in the end # Thus the data will be limited to only what we need. if not sets[obj_set]["members"]: self.log.debug( "Removing redundant set information: {}".format(obj_set) ) sets.pop(obj_set, None) self.log.debug("Gathering attribute changes to instance members..") attributes = self.collect_attributes_changed(instance) # Store data on the instance instance.data["lookData"] = { "attributes": attributes, "relationships": sets } # Collect file nodes used by shading engines (if we have any) files = [] look_sets = list(sets.keys()) shader_attrs = [ "surfaceShader", "volumeShader", "displacementShader", "aiSurfaceShader", "aiVolumeShader", "rman__surface", "rman__displacement" ] if look_sets: self.log.debug("Found look sets: {}".format(look_sets)) # Get all material attrs for all look sets to retrieve their inputs existing_attrs = [] for look in look_sets: for attr in shader_attrs: if cmds.attributeQuery(attr, node=look, exists=True): existing_attrs.append("{}.{}".format(look, attr)) materials = cmds.listConnections(existing_attrs, source=True, destination=False) or [] self.log.debug("Found materials:\n{}".format(materials)) self.log.debug("Found the following sets:\n{}".format(look_sets)) # Get the entire node chain of the look sets # history = cmds.listHistory(look_sets, allConnections=True) # if materials list is empty, listHistory() will crash with # RuntimeError history = set() if materials: history = set( cmds.listHistory(materials, allConnections=True)) # Since we retrieved history only of the connected materials # connected to the look sets above we now add direct history # for some of the look sets directly # handling render attribute sets # Maya (at least 2024) crashes with Warning when render set type # isn't available. cmds.ls() will return empty list if RENDER_SET_TYPES: render_sets = cmds.ls(look_sets, type=RENDER_SET_TYPES) if render_sets: history.update( cmds.listHistory(render_sets, future=False, pruneDagObjects=True) or [] ) # Ensure unique entries only history = list(history) files = cmds.ls(history, # It's important only node types are passed that # exist (e.g. for loaded plugins) because otherwise # the result will turn back empty type=list(FILE_NODES.keys()), long=True) # Sort for log readability files.sort() self.log.debug("Collected file nodes:\n{}".format(files)) # Collect textures if any file nodes are found resources = [] for node in files: # sort for log readability resources.extend(self.collect_resources(node)) instance.data["resources"] = resources self.log.debug("Collected resources: {}".format(resources)) # Log warning when no relevant sets were retrieved for the look. if ( not instance.data["lookData"]["relationships"] and "model" not in self.families ): self.log.warning("No sets found for the nodes in the " "instance: %s" % instance[:]) # Ensure unique shader sets # Add shader sets to the instance for unify ID validation instance.extend(shader for shader in look_sets if shader not in instance_lookup) self.log.debug("Collected look for %s" % instance) def collect_sets(self, instance): """Collect all objectSets which are of importance for publishing It checks if all nodes in the instance are related to any objectSet which need to be Args: instance (list): all nodes to be published Returns: dict """ sets = {} for node in instance: related_sets = lib.get_related_sets(node) if not related_sets: continue for objset in related_sets: if objset in sets: continue sets[objset] = {"uuid": lib.get_id(objset), "members": list()} return sets def collect_member_data(self, member, instance_members): """Get all information of the node Args: member (str): the name of the node to check instance_members (set): the collected instance members Returns: dict """ node, components = (member.rsplit(".", 1) + [None])[:2] # Only include valid members of the instance if node not in instance_members: return node_id = lib.get_id(node) if not node_id: self.log.error("Member '{}' has no attribute 'cbId'".format(node)) return member_data = {"name": node, "uuid": node_id} if components: member_data["components"] = components return member_data def collect_attributes_changed(self, instance): """Collect all userDefined attributes which have changed Each node gets checked for user defined attributes which have been altered during development. Each changes gets logged in a dictionary [{name: node, uuid: uuid, attributes: {attribute: value}}] Args: instance (list): all nodes which will be published Returns: list """ attributes = [] for node in instance: # Collect changes to "custom" attributes node_attrs = get_look_attrs(node) # Only include if there are any properties we care about if not node_attrs: continue self.log.debug( "Node \"{0}\" attributes: {1}".format(node, node_attrs) ) node_attributes = {} for attr in node_attrs: if not cmds.attributeQuery(attr, node=node, exists=True): continue attribute = "{}.{}".format(node, attr) # We don't support mixed-type attributes yet. if cmds.attributeQuery(attr, node=node, multi=True): self.log.warning("Attribute '{}' is mixed-type and is " "not supported yet.".format(attribute)) continue if cmds.getAttr(attribute, type=True) == "message": continue node_attributes[attr] = cmds.getAttr(attribute, asString=True) # Only include if there are any properties we care about if not node_attributes: continue attributes.append({"name": node, "uuid": lib.get_id(node), "attributes": node_attributes}) return attributes def collect_resources(self, node): """Collect the link to the file(s) used (resource) Args: node (str): name of the node Returns: dict """ if cmds.nodeType(node) not in FILE_NODES: self.log.error( "Unsupported file node: {}".format(cmds.nodeType(node))) raise AssertionError("Unsupported file node") self.log.debug( "Collecting resource: {} ({})".format(node, cmds.nodeType(node)) ) attributes = get_attributes(FILE_NODES, cmds.nodeType(node), node) for attribute in attributes: source = cmds.getAttr("{}.{}".format( node, attribute )) self.log.debug(" - file source: {}".format(source)) color_space_attr = "{}.colorSpace".format(node) try: color_space = cmds.getAttr(color_space_attr) except ValueError: # node doesn't have colorspace attribute color_space = "Raw" # Compare with the computed file path, e.g. the one with # the pattern in it, to generate some logging information # about this difference # Only for file nodes with `fileTextureName` attribute if attribute == "fileTextureName": computed_source = cmds.getAttr( "{}.computedFileTextureNamePattern".format(node) ) if source != computed_source: self.log.debug("Detected computed file pattern difference " "from original pattern: {0} " "({1} -> {2})".format(node, source, computed_source)) # renderman allows nodes to have filename attribute empty while # you can have another incoming connection from different node. if not source and cmds.nodeType(node) in PXR_NODES: self.log.debug("Renderman: source is empty, skipping...") continue # We replace backslashes with forward slashes because V-Ray # can't handle the UDIM files with the backslashes in the # paths as the computed patterns source = source.replace("\\", "/") files = get_file_node_files(node) if len(files) == 0: self.log.debug("No valid files found from node `%s`" % node) self.log.debug("collection of resource done:") self.log.debug(" - node: {}".format(node)) self.log.debug(" - attribute: {}".format(attribute)) self.log.debug(" - source: {}".format(source)) self.log.debug(" - file: {}".format(files)) self.log.debug(" - color space: {}".format(color_space)) # Define the resource yield { "node": node, # here we are passing not only attribute, but with node again # this should be simplified and changed extractor. "attribute": "{}.{}".format(node, attribute), "source": source, # required for resources "files": files, "color_space": color_space } # required for resources class CollectModelRenderSets(CollectLook): """Collect render attribute sets for model instance. Collects additional render attribute sets so they can be published with model. """ order = pyblish.api.CollectorOrder + 0.21 families = ["model"] label = "Collect Model Render Sets" hosts = ["maya"] maketx = True def collect_sets(self, instance): """Collect all related objectSets except shadingEngines Args: instance (list): all nodes to be published Returns: dict """ sets = {} for node in instance: related_sets = lib.get_related_sets(node) if not related_sets: continue for objset in related_sets: if objset in sets: continue if "shadingEngine" in cmds.nodeType(objset, inherited=True): continue sets[objset] = {"uuid": lib.get_id(objset), "members": list()} return sets ================================================ FILE: openpype/hosts/maya/plugins/publish/collect_maya_scene_time.py ================================================ from maya import cmds import pyblish.api class CollectMayaSceneTime(pyblish.api.InstancePlugin): """Collect Maya Scene playback range This allows to reproduce the playback range for the content to be loaded. It does *not* limit the extracted data to only data inside that time range. """ order = pyblish.api.CollectorOrder + 0.2 label = 'Collect Maya Scene Time' families = ["mayaScene"] def process(self, instance): instance.data.update({ "frameStart": int( cmds.playbackOptions(query=True, minTime=True)), "frameEnd": int( cmds.playbackOptions(query=True, maxTime=True)), "frameStartHandle": int( cmds.playbackOptions(query=True, animationStartTime=True)), "frameEndHandle": int( cmds.playbackOptions(query=True, animationEndTime=True)) }) ================================================ FILE: openpype/hosts/maya/plugins/publish/collect_maya_units.py ================================================ import maya.cmds as cmds import maya.mel as mel import pyblish.api class CollectMayaUnits(pyblish.api.ContextPlugin): """Collect Maya's scene units.""" label = "Maya Units" order = pyblish.api.CollectorOrder hosts = ["maya"] def process(self, context): # Get the current linear units units = cmds.currentUnit(query=True, linear=True) # Get the current angular units ('deg' or 'rad') units_angle = cmds.currentUnit(query=True, angle=True) # Get the current time units # Using the mel command is simpler than using # `cmds.currentUnit(q=1, time=1)`. Otherwise we # have to parse the returned string value to FPS fps = mel.eval('currentTimeUnitToFPS()') context.data['linearUnits'] = units context.data['angularUnits'] = units_angle context.data['fps'] = fps ================================================ FILE: openpype/hosts/maya/plugins/publish/collect_maya_workspace.py ================================================ import os import pyblish.api from maya import cmds class CollectMayaWorkspace(pyblish.api.ContextPlugin): """Inject the current workspace into context""" order = pyblish.api.CollectorOrder - 0.5 label = "Maya Workspace" hosts = ['maya'] def process(self, context): workspace = cmds.workspace(rootDirectory=True, query=True) if not workspace: # Project has not been set. Files will # instead end up next to the working file. workspace = cmds.workspace(dir=True, query=True) # Maya returns forward-slashes by default normalised = os.path.normpath(workspace) context.set_data('workspaceDir', value=normalised) ================================================ FILE: openpype/hosts/maya/plugins/publish/collect_model.py ================================================ from maya import cmds import pyblish.api class CollectModelData(pyblish.api.InstancePlugin): """Collect model data Ensures always only a single frame is extracted (current frame). Note: This is a workaround so that the `pype.model` family can use the same pointcache extractor implementation as animation and pointcaches. This always enforces the "current" frame to be published. """ order = pyblish.api.CollectorOrder + 0.2 label = 'Collect Model Data' families = ["model"] def process(self, instance): # Extract only current frame (override) frame = cmds.currentTime(query=True) instance.data["frameStart"] = frame instance.data["frameEnd"] = frame ================================================ FILE: openpype/hosts/maya/plugins/publish/collect_multiverse_look.py ================================================ import glob import os import re from maya import cmds import pyblish.api from openpype.hosts.maya.api import lib SHAPE_ATTRS = ["castsShadows", "receiveShadows", "motionBlur", "primaryVisibility", "smoothShading", "visibleInReflections", "visibleInRefractions", "doubleSided", "opposite"] SHAPE_ATTRS = set(SHAPE_ATTRS) COLOUR_SPACES = ['sRGB', 'linear', 'auto'] MIPMAP_EXTENSIONS = ['tdl'] class _NodeTypeAttrib(object): """docstring for _NodeType""" def __init__(self, name, fname, computed_fname=None, colour_space=None): self.name = name self.fname = fname self.computed_fname = computed_fname or fname self.colour_space = colour_space or "colorSpace" def get_fname(self, node): return "{}.{}".format(node, self.fname) def get_computed_fname(self, node): return "{}.{}".format(node, self.computed_fname) def get_colour_space(self, node): return "{}.{}".format(node, self.colour_space) def __str__(self): return "_NodeTypeAttrib(name={}, fname={}, " "computed_fname={}, colour_space={})".format( self.name, self.fname, self.computed_fname, self.colour_space) NODETYPES = { "file": [_NodeTypeAttrib("file", "fileTextureName", "computedFileTextureNamePattern")], "aiImage": [_NodeTypeAttrib("aiImage", "filename")], "RedshiftNormalMap": [_NodeTypeAttrib("RedshiftNormalMap", "tex0")], "dlTexture": [_NodeTypeAttrib("dlTexture", "textureFile", None, "textureFile_meta_colorspace")], "dlTriplanar": [_NodeTypeAttrib("dlTriplanar", "colorTexture", None, "colorTexture_meta_colorspace"), _NodeTypeAttrib("dlTriplanar", "floatTexture", None, "floatTexture_meta_colorspace"), _NodeTypeAttrib("dlTriplanar", "heightTexture", None, "heightTexture_meta_colorspace")] } def get_file_paths_for_node(node): """Gets all the file paths in this node. Returns all filepaths that this node references. Some node types only reference one, but others, like dlTriplanar, can reference 3. Args: node (str): Name of the Maya node Returns list(str): A list with all evaluated maya attributes for filepaths. """ node_type = cmds.nodeType(node) if node_type not in NODETYPES: return [] paths = [] for node_type_attr in NODETYPES[node_type]: fname = cmds.getAttr("{}.{}".format(node, node_type_attr.fname)) paths.append(fname) return paths def node_uses_image_sequence(node): """Return whether file node uses an image sequence or single image. Determine if a node uses an image sequence or just a single image, not always obvious from its file path alone. Args: node (str): Name of the Maya node Returns: bool: True if node uses an image sequence """ # useFrameExtension indicates an explicit image sequence paths = get_file_node_paths(node) paths = [path.lower() for path in paths] # The following tokens imply a sequence patterns = [" ", " ", " ", "u_v ", " .tif will return as /path/to/texture.*.tif. Args: path (str): the image sequence path Returns: str: Return glob string that matches the filename pattern. """ if path is None: return path # If any of the patterns, convert the pattern patterns = { " ": " ", " ": " ", " ": " ", "#": "#", "u_v ": "| ", " ", # noqa - copied from collect_look.py " ": " " } lower = path.lower() has_pattern = False for pattern, regex_pattern in patterns.items(): if pattern in lower: path = re.sub(regex_pattern, "*", path, flags=re.IGNORECASE) has_pattern = True if has_pattern: return path base = os.path.basename(path) matches = list(re.finditer(r'\d+', base)) if matches: match = matches[-1] new_base = '{0}*{1}'.format(base[:match.start()], base[match.end():]) head = os.path.dirname(path) return os.path.join(head, new_base) else: return path def get_file_node_paths(node): """Get the file path used by a Maya file node. Args: node (str): Name of the Maya file node Returns: str: the file path in use """ # if the path appears to be sequence, use computedFileTextureNamePattern, # this preserves the <> tag if cmds.attributeQuery('computedFileTextureNamePattern', node=node, exists=True): plug = '{0}.computedFileTextureNamePattern'.format(node) texture_pattern = cmds.getAttr(plug) patterns = [" ", " ", "u_v ", " ", " "] lower = texture_pattern.lower() if any(pattern in lower for pattern in patterns): return [texture_pattern] return get_file_paths_for_node(node) def get_file_node_files(node): """Return the file paths related to the file node Note: Will only return existing files. Returns an empty list if not valid existing files are linked. Returns: list: List of full file paths. """ paths = get_file_node_paths(node) paths = [cmds.workspace(expandName=path) for path in paths] if node_uses_image_sequence(node): globs = [] for path in paths: globs += glob.glob(seq_to_glob(path)) return globs else: return list(filter(lambda x: os.path.exists(x), paths)) def get_mipmap(fname): for colour_space in COLOUR_SPACES: for mipmap_ext in MIPMAP_EXTENSIONS: mipmap_fname = '.'.join([fname, colour_space, mipmap_ext]) if os.path.exists(mipmap_fname): return mipmap_fname return None def is_mipmap(fname): ext = os.path.splitext(fname)[1][1:] if ext in MIPMAP_EXTENSIONS: return True return False class CollectMultiverseLookData(pyblish.api.InstancePlugin): """Collect Multiverse Look Searches through the overrides finding all material overrides. From there it extracts the shading group and then finds all texture files in the shading group network. It also checks for mipmap versions of texture files and adds them to the resources to get published. """ order = pyblish.api.CollectorOrder + 0.2 label = 'Collect Multiverse Look' families = ["mvLook"] def process(self, instance): # Load plugin first cmds.loadPlugin("MultiverseForMaya", quiet=True) import multiverse self.log.debug("Processing mvLook for '{}'".format(instance)) nodes = set() for node in instance: # We want only mvUsdCompoundShape nodes. nodes_of_interest = cmds.ls(node, dag=True, shapes=False, type="mvUsdCompoundShape", noIntermediate=True, long=True) nodes.update(nodes_of_interest) sets = {} instance.data["resources"] = [] publishMipMap = instance.data["publishMipMap"] for node in nodes: self.log.debug("Getting resources for '{}'".format(node)) # We know what nodes need to be collected, now we need to # extract the materials overrides. overrides = multiverse.ListMaterialOverridePrims(node) for override in overrides: matOver = multiverse.GetMaterialOverride(node, override) if isinstance(matOver, multiverse.MaterialSourceShadingGroup): # We now need to grab the shadingGroup so add it to the # sets we pass down the pipe. shadingGroup = matOver.shadingGroupName self.log.debug("ShadingGroup = '{}'".format(shadingGroup)) sets[shadingGroup] = {"uuid": lib.get_id( shadingGroup), "members": list()} # The SG may reference files, add those too! history = cmds.listHistory( shadingGroup, allConnections=True) # We need to iterate over node_types since `cmds.ls` may # error out if we don't have the appropriate plugin loaded. files = [] for node_type in NODETYPES.keys(): files += cmds.ls(history, type=node_type, long=True) for f in files: resources = self.collect_resource(f, publishMipMap) instance.data["resources"] += resources elif isinstance(matOver, multiverse.MaterialSourceUsdPath): # TODO: Handle this later. pass # Store data on the instance for validators, extractos, etc. instance.data["lookData"] = { "attributes": [], "relationships": sets } def collect_resource(self, node, publishMipMap): """Collect the link to the file(s) used (resource) Args: node (str): name of the node Returns: dict """ node_type = cmds.nodeType(node) self.log.debug("processing: {}/{}".format(node, node_type)) if node_type not in NODETYPES: self.log.error("Unsupported file node: {}".format(node_type)) raise AssertionError("Unsupported file node") resources = [] for node_type_attr in NODETYPES[node_type]: fname_attrib = node_type_attr.get_fname(node) computed_fname_attrib = node_type_attr.get_computed_fname(node) colour_space_attrib = node_type_attr.get_colour_space(node) source = cmds.getAttr(fname_attrib) color_space = "Raw" try: color_space = cmds.getAttr(colour_space_attrib) except ValueError: # node doesn't have colorspace attribute, use "Raw" from before pass # Compare with the computed file path, e.g. the one with the # pattern in it, to generate some logging information about this # difference # computed_attribute = "{}.computedFileTextureNamePattern".format(node) # noqa computed_source = cmds.getAttr(computed_fname_attrib) if source != computed_source: self.log.debug("Detected computed file pattern difference " "from original pattern: {0} " "({1} -> {2})".format(node, source, computed_source)) # We replace backslashes with forward slashes because V-Ray # can't handle the UDIM files with the backslashes in the # paths as the computed patterns source = source.replace("\\", "/") files = get_file_node_files(node) files = self.handle_files(files, publishMipMap) if len(files) == 0: self.log.error("No valid files found from node `%s`" % node) self.log.debug("collection of resource done:") self.log.debug(" - node: {}".format(node)) self.log.debug(" - attribute: {}".format(fname_attrib)) self.log.debug(" - source: {}".format(source)) self.log.debug(" - file: {}".format(files)) self.log.debug(" - color space: {}".format(color_space)) # Define the resource resource = {"node": node, "attribute": fname_attrib, "source": source, # required for resources "files": files, "color_space": color_space} # required for resources resources.append(resource) return resources def handle_files(self, files, publishMipMap): """This will go through all the files and make sure that they are either already mipmapped or have a corresponding mipmap sidecar and add that to the list.""" if not publishMipMap: return files extra_files = [] self.log.debug("Expecting MipMaps, going to look for them.") for fname in files: self.log.debug("Checking '{}' for mipmaps".format(fname)) if is_mipmap(fname): self.log.debug(" - file is already MipMap, skipping.") continue mipmap = get_mipmap(fname) if mipmap: self.log.debug(" mipmap found for '{}'".format(fname)) extra_files.append(mipmap) else: self.log.warning(" no mipmap found for '{}'".format(fname)) return files + extra_files ================================================ FILE: openpype/hosts/maya/plugins/publish/collect_pointcache.py ================================================ from maya import cmds import pyblish.api class CollectPointcache(pyblish.api.InstancePlugin): """Collect pointcache data for instance.""" order = pyblish.api.CollectorOrder + 0.4 families = ["pointcache"] label = "Collect Pointcache" hosts = ["maya"] def process(self, instance): if instance.data.get("farm"): instance.data["families"].append("publish.farm") proxy_set = None for node in cmds.ls(instance.data["setMembers"], exactType="objectSet"): # Find proxy_SET objectSet in the instance for proxy meshes if node.endswith("proxy_SET"): members = cmds.sets(node, query=True) if members is None: self.log.debug("Skipped empty proxy_SET: \"%s\" " % node) continue self.log.debug("Found proxy set: {}".format(node)) proxy_set = node instance.data["proxy"] = [] instance.data["proxyRoots"] = [] for member in members: instance.data["proxy"].extend(cmds.ls(member, long=True)) instance.data["proxyRoots"].extend( cmds.ls(member, long=True) ) instance.data["proxy"].extend( cmds.listRelatives(member, shapes=True, fullPath=True) ) self.log.debug( "Found proxy members: {}".format(instance.data["proxy"]) ) break if proxy_set: instance.remove(proxy_set) instance.data["setMembers"].remove(proxy_set) # User defined attributes. instance.data["includeUserDefinedAttributes"] = ( instance.data["creator_attributes"]["includeUserDefinedAttributes"] ) ================================================ FILE: openpype/hosts/maya/plugins/publish/collect_remove_marked.py ================================================ import pyblish.api class CollectRemoveMarked(pyblish.api.ContextPlugin): """Remove marked data Remove instances that have 'remove' in their instance.data """ order = pyblish.api.CollectorOrder + 0.499 label = 'Remove Marked Instances' def process(self, context): self.log.debug(context) # make ftrack publishable instances_to_remove = [] for instance in context: if instance.data.get('remove'): instances_to_remove.append(instance) for instance in instances_to_remove: context.remove(instance) ================================================ FILE: openpype/hosts/maya/plugins/publish/collect_render.py ================================================ # -*- coding: utf-8 -*- """Collect render data. This collector will go through render layers in maya and prepare all data needed to create instances and their representations for submission and publishing on farm. Requires: instance -> families instance -> setMembers context -> currentFile context -> workspaceDir context -> user session -> AVALON_ASSET Optional: Provides: instance -> label instance -> subset instance -> attachTo instance -> setMembers instance -> publish instance -> frameStart instance -> frameEnd instance -> byFrameStep instance -> renderer instance -> family instance -> families instance -> asset instance -> time instance -> author instance -> source instance -> expectedFiles instance -> resolutionWidth instance -> resolutionHeight instance -> pixelAspect """ import os import platform import json from maya import cmds import pyblish.api from openpype.pipeline import KnownPublishError from openpype.lib import get_formatted_current_time from openpype.hosts.maya.api.lib_renderproducts import ( get as get_layer_render_products, UnsupportedRendererException ) from openpype.hosts.maya.api import lib class CollectMayaRender(pyblish.api.InstancePlugin): """Gather all publishable render layers from renderSetup.""" order = pyblish.api.CollectorOrder + 0.01 hosts = ["maya"] families = ["renderlayer"] label = "Collect Render Layers" sync_workfile_version = False _aov_chars = { "dot": ".", "dash": "-", "underscore": "_" } def process(self, instance): # TODO: Re-add force enable of workfile instance? # TODO: Re-add legacy layer support with LAYER_ prefix but in Creator # TODO: Set and collect active state of RenderLayer in Creator using # renderlayer.isRenderable() context = instance.context layer = instance.data["transientData"]["layer"] objset = instance.data.get("instance_node") filepath = context.data["currentFile"].replace("\\", "/") workspace = context.data["workspaceDir"] # check if layer is renderable if not layer.isRenderable(): msg = "Render layer [ {} ] is not " "renderable".format( layer.name() ) self.log.warning(msg) # detect if there are sets (subsets) to attach render to sets = cmds.sets(objset, query=True) or [] attach_to = [] for s in sets: if not cmds.attributeQuery("family", node=s, exists=True): continue attach_to.append( { "version": None, # we need integrator for that "subset": s, "family": cmds.getAttr("{}.family".format(s)), } ) self.log.debug(" -> attach render to: {}".format(s)) layer_name = layer.name() # collect all frames we are expecting to be rendered # return all expected files for all cameras and aovs in given # frame range try: layer_render_products = get_layer_render_products(layer.name()) except UnsupportedRendererException as exc: raise KnownPublishError(exc) render_products = layer_render_products.layer_data.products assert render_products, "no render products generated" expected_files = [] multipart = False for product in render_products: if product.multipart: multipart = True product_name = product.productName if product.camera and layer_render_products.has_camera_token(): product_name = "{}{}".format( product.camera, "_{}".format(product_name) if product_name else "") expected_files.append( { product_name: layer_render_products.get_files( product) }) has_cameras = any(product.camera for product in render_products) assert has_cameras, "No render cameras found." self.log.debug("multipart: {}".format( multipart)) assert expected_files, "no file names were generated, this is a bug" self.log.debug( "expected files: {}".format( json.dumps(expected_files, indent=4, sort_keys=True) ) ) # if we want to attach render to subset, check if we have AOV's # in expectedFiles. If so, raise error as we cannot attach AOV # (considered to be subset on its own) to another subset if attach_to: assert isinstance(expected_files, list), ( "attaching multiple AOVs or renderable cameras to " "subset is not supported" ) # append full path aov_dict = {} image_directory = os.path.join( cmds.workspace(query=True, rootDirectory=True), cmds.workspace(fileRuleEntry="images") ) # replace relative paths with absolute. Render products are # returned as list of dictionaries. publish_meta_path = None for aov in expected_files: full_paths = [] aov_first_key = list(aov.keys())[0] for file in aov[aov_first_key]: full_path = os.path.join(image_directory, file) full_path = full_path.replace("\\", "/") full_paths.append(full_path) publish_meta_path = os.path.dirname(full_path) aov_dict[aov_first_key] = full_paths full_exp_files = [aov_dict] self.log.debug(full_exp_files) if publish_meta_path is None: raise KnownPublishError("Unable to detect any expected output " "images for: {}. Make sure you have a " "renderable camera and a valid frame " "range set for your renderlayer." "".format(instance.name)) frame_start_render = int(self.get_render_attribute( "startFrame", layer=layer_name)) frame_end_render = int(self.get_render_attribute( "endFrame", layer=layer_name)) if (int(context.data["frameStartHandle"]) == frame_start_render and int(context.data["frameEndHandle"]) == frame_end_render): # noqa: W503, E501 handle_start = context.data["handleStart"] handle_end = context.data["handleEnd"] frame_start = context.data["frameStart"] frame_end = context.data["frameEnd"] frame_start_handle = context.data["frameStartHandle"] frame_end_handle = context.data["frameEndHandle"] else: handle_start = 0 handle_end = 0 frame_start = frame_start_render frame_end = frame_end_render frame_start_handle = frame_start_render frame_end_handle = frame_end_render # find common path to store metadata # so if image prefix is branching to many directories # metadata file will be located in top-most common # directory. # TODO: use `os.path.commonpath()` after switch to Python 3 publish_meta_path = os.path.normpath(publish_meta_path) common_publish_meta_path = os.path.splitdrive( publish_meta_path)[0] if common_publish_meta_path: common_publish_meta_path += os.path.sep for part in publish_meta_path.replace( common_publish_meta_path, "").split(os.path.sep): common_publish_meta_path = os.path.join( common_publish_meta_path, part) if part == layer_name: break # TODO: replace this terrible linux hotfix with real solution :) if platform.system().lower() in ["linux", "darwin"]: common_publish_meta_path = "/" + common_publish_meta_path self.log.debug( "Publish meta path: {}".format(common_publish_meta_path)) # Get layer specific settings, might be overrides colorspace_data = lib.get_color_management_preferences() data = { "farm": True, "attachTo": attach_to, "multipartExr": multipart, "review": instance.data.get("review") or False, # Frame range "handleStart": handle_start, "handleEnd": handle_end, "frameStart": frame_start, "frameEnd": frame_end, "frameStartHandle": frame_start_handle, "frameEndHandle": frame_end_handle, "byFrameStep": int( self.get_render_attribute("byFrameStep", layer=layer_name)), # Renderlayer "renderer": self.get_render_attribute( "currentRenderer", layer=layer_name).lower(), "setMembers": layer._getLegacyNodeName(), # legacy renderlayer "renderlayer": layer_name, # todo: is `time` and `author` still needed? "time": get_formatted_current_time(), "author": context.data["user"], # Add source to allow tracing back to the scene from # which was submitted originally "source": filepath, "expectedFiles": full_exp_files, "publishRenderMetadataFolder": common_publish_meta_path, "renderProducts": layer_render_products, "resolutionWidth": lib.get_attr_in_layer( "defaultResolution.width", layer=layer_name ), "resolutionHeight": lib.get_attr_in_layer( "defaultResolution.height", layer=layer_name ), "pixelAspect": lib.get_attr_in_layer( "defaultResolution.pixelAspect", layer=layer_name ), # todo: Following are likely not needed due to collecting from the # instance itself if they are attribute definitions "tileRendering": instance.data.get("tileRendering") or False, # noqa: E501 "tilesX": instance.data.get("tilesX") or 2, "tilesY": instance.data.get("tilesY") or 2, "convertToScanline": instance.data.get( "convertToScanline") or False, "useReferencedAovs": instance.data.get( "useReferencedAovs") or instance.data.get( "vrayUseReferencedAovs") or False, "aovSeparator": layer_render_products.layer_data.aov_separator, # noqa: E501 "renderSetupIncludeLights": instance.data.get( "renderSetupIncludeLights" ), "colorspaceConfig": colorspace_data["config"], "colorspaceDisplay": colorspace_data["display"], "colorspaceView": colorspace_data["view"], } rr_settings = ( context.data["system_settings"]["modules"]["royalrender"] ) if rr_settings["enabled"]: data["rrPathName"] = instance.data.get("rrPathName") self.log.debug(data["rrPathName"]) if self.sync_workfile_version: data["version"] = context.data["version"] for _instance in context: if _instance.data['family'] == "workfile": _instance.data["version"] = context.data["version"] # Define nice label label = "{0} ({1})".format(layer_name, instance.data["asset"]) label += " [{0}-{1}]".format( int(data["frameStartHandle"]), int(data["frameEndHandle"]) ) data["label"] = label # Override frames should be False if extendFrames is False. This is # to ensure it doesn't go off doing crazy unpredictable things extend_frames = instance.data.get("extendFrames", False) if not extend_frames: instance.data["overrideExistingFrame"] = False # Update the instace instance.data.update(data) @staticmethod def get_render_attribute(attr, layer): """Get attribute from render options. Args: attr (str): name of attribute to be looked up layer (str): name of render layer Returns: Attribute value """ return lib.get_attr_in_layer( "defaultRenderGlobals.{}".format(attr), layer=layer ) ================================================ FILE: openpype/hosts/maya/plugins/publish/collect_render_layer_aovs.py ================================================ from maya import cmds import pyblish.api from openpype.hosts.maya.api import lib class CollectRenderLayerAOVS(pyblish.api.InstancePlugin): """Collect all render layer's AOVs / Render Elements that will render. This collector is important to be able to Extend Frames. Technical information: Each renderer uses different logic to work with render passes. VRay - RenderElement Simple node connection to the actual renderLayer node Arnold - AOV: Uses its own render settings node and connects an aiOAV to it Redshift - AOV: Uses its own render settings node and RedshiftAOV node. It is not connected but all AOVs are enabled for all render layers by default. """ order = pyblish.api.CollectorOrder + 0.02 label = "Render Elements / AOVs" hosts = ["maya"] families = ["renderlayer"] def process(self, instance): # Check if Extend Frames is toggled if not instance.data("extendFrames", False): return # Get renderer renderer = instance.data["renderer"] self.log.debug("Renderer found: {}".format(renderer)) rp_node_types = {"vray": ["VRayRenderElement", "VRayRenderElementSet"], "arnold": ["aiAOV"], "redshift": ["RedshiftAOV"]} if renderer not in rp_node_types.keys(): self.log.error("Unsupported renderer found: '{}'".format(renderer)) return result = [] # Collect all AOVs / Render Elements layer = instance.data["renderlayer"] node_type = rp_node_types[renderer] render_elements = cmds.ls(type=node_type) # Check if AOVs / Render Elements are enabled for element in render_elements: enabled = lib.get_attr_in_layer("{}.enabled".format(element), layer=layer) if not enabled: continue pass_name = self.get_pass_name(renderer, element) render_pass = "%s.%s" % (instance.data["subset"], pass_name) result.append(render_pass) self.log.debug("Found {} render elements / AOVs for " "'{}'".format(len(result), instance.data["subset"])) instance.data["renderPasses"] = result def get_pass_name(self, renderer, node): if renderer == "vray": # Get render element pass type vray_node_attr = next(attr for attr in cmds.listAttr(node) if attr.startswith("vray_name")) pass_type = vray_node_attr.rsplit("_", 1)[-1] # Support V-Ray extratex explicit name (if set by user) if pass_type == "extratex": explicit_attr = "{}.vray_explicit_name_extratex".format(node) explicit_name = cmds.getAttr(explicit_attr) if explicit_name: return explicit_name # Node type is in the attribute name but we need to check if value # of the attribute as it can be changed return cmds.getAttr("{}.{}".format(node, vray_node_attr)) elif renderer in ["arnold", "redshift"]: return cmds.getAttr("{}.name".format(node)) else: raise RuntimeError("Unsupported renderer: '{}'".format(renderer)) ================================================ FILE: openpype/hosts/maya/plugins/publish/collect_renderable_camera.py ================================================ import pyblish.api from maya import cmds from openpype.hosts.maya.api.lib_rendersetup import get_attr_in_layer class CollectRenderableCamera(pyblish.api.InstancePlugin): """Collect the renderable camera(s) for the render layer""" # Offset to be after renderlayer collection. order = pyblish.api.CollectorOrder + 0.02 label = "Collect Renderable Camera(s)" hosts = ["maya"] families = ["vrayscene_layer", "renderlayer"] def process(self, instance): if "vrayscene_layer" in instance.data.get("families", []): layer = instance.data.get("layer") else: layer = instance.data["renderlayer"] cameras = cmds.ls(type="camera", long=True) renderable = [cam for cam in cameras if get_attr_in_layer("{}.renderable".format(cam), layer)] self.log.debug( "Found renderable cameras %s: %s", len(renderable), renderable ) instance.data["cameras"] = renderable ================================================ FILE: openpype/hosts/maya/plugins/publish/collect_review.py ================================================ from maya import cmds, mel import pyblish.api from openpype.client import get_subset_by_name from openpype.pipeline import KnownPublishError from openpype.hosts.maya.api import lib class CollectReview(pyblish.api.InstancePlugin): """Collect Review data """ order = pyblish.api.CollectorOrder + 0.3 label = 'Collect Review Data' families = ["review"] def process(self, instance): # Get panel. instance.data["panel"] = cmds.playblast( activeEditor=True ).rsplit("|", 1)[-1] # get cameras members = instance.data['setMembers'] self.log.debug('members: {}'.format(members)) cameras = cmds.ls(members, long=True, dag=True, cameras=True) camera = cameras[0] if cameras else None context = instance.context objectset = { i.data.get("instance_node") for i in context } # Collect display lights. display_lights = instance.data.get("displayLights", "default") if display_lights == "project_settings": settings = instance.context.data["project_settings"] settings = settings["maya"]["publish"]["ExtractPlayblast"] settings = settings["capture_preset"]["Viewport Options"] display_lights = settings["displayLights"] # Collect camera focal length. burninDataMembers = instance.data.get("burninDataMembers", {}) if camera is not None: attr = camera + ".focalLength" if lib.get_attribute_input(attr): start = instance.data["frameStart"] end = instance.data["frameEnd"] + 1 time_range = range(int(start), int(end)) focal_length = [cmds.getAttr(attr, time=t) for t in time_range] else: focal_length = cmds.getAttr(attr) burninDataMembers["focalLength"] = focal_length # Account for nested instances like model. reviewable_subsets = list(set(members) & objectset) if reviewable_subsets: if len(reviewable_subsets) > 1: raise KnownPublishError( "Multiple attached subsets for review are not supported. " "Attached: {}".format(", ".join(reviewable_subsets)) ) reviewable_subset = reviewable_subsets[0] self.log.debug( "Subset attached to review: {}".format(reviewable_subset) ) # Find the relevant publishing instance in the current context reviewable_inst = next(inst for inst in context if inst.name == reviewable_subset) data = reviewable_inst.data self.log.debug( 'Adding review family to {}'.format(reviewable_subset) ) if data.get('families'): data['families'].append('review') else: data['families'] = ['review'] data["cameras"] = cameras data['review_camera'] = camera data['frameStartFtrack'] = instance.data["frameStartHandle"] data['frameEndFtrack'] = instance.data["frameEndHandle"] data['frameStartHandle'] = instance.data["frameStartHandle"] data['frameEndHandle'] = instance.data["frameEndHandle"] data['handleStart'] = instance.data["handleStart"] data['handleEnd'] = instance.data["handleEnd"] data["frameStart"] = instance.data["frameStart"] data["frameEnd"] = instance.data["frameEnd"] data['step'] = instance.data['step'] # this (with other time related data) should be set on # representations. Once plugins like Extract Review start # using representations, this should be removed from here # as Extract Playblast is already adding fps to representation. data['fps'] = context.data['fps'] data['review_width'] = instance.data['review_width'] data['review_height'] = instance.data['review_height'] data["isolate"] = instance.data["isolate"] data["panZoom"] = instance.data.get("panZoom", False) data["panel"] = instance.data["panel"] data["displayLights"] = display_lights data["burninDataMembers"] = burninDataMembers for key, value in instance.data["publish_attributes"].items(): data["publish_attributes"][key] = value # The review instance must be active cmds.setAttr(str(instance) + '.active', 1) instance.data['remove'] = True else: project_name = instance.context.data["projectName"] asset_doc = instance.context.data['assetEntity'] task = instance.context.data["task"] legacy_subset_name = task + 'Review' subset_doc = get_subset_by_name( project_name, legacy_subset_name, asset_doc["_id"], fields=["_id"] ) if subset_doc: self.log.debug("Existing subsets found, keep legacy name.") instance.data['subset'] = legacy_subset_name instance.data["cameras"] = cameras instance.data['review_camera'] = camera instance.data['frameStartFtrack'] = \ instance.data["frameStartHandle"] instance.data['frameEndFtrack'] = \ instance.data["frameEndHandle"] instance.data["displayLights"] = display_lights instance.data["burninDataMembers"] = burninDataMembers # this (with other time related data) should be set on # representations. Once plugins like Extract Review start # using representations, this should be removed from here # as Extract Playblast is already adding fps to representation. instance.data["fps"] = instance.context.data["fps"] # make ftrack publishable instance.data.setdefault("families", []).append('ftrack') cmds.setAttr(str(instance) + '.active', 1) # Collect audio playback_slider = mel.eval('$tmpVar=$gPlayBackSlider') audio_name = cmds.timeControl(playback_slider, query=True, sound=True) display_sounds = cmds.timeControl( playback_slider, query=True, displaySound=True ) def get_audio_node_data(node): return { "offset": cmds.getAttr("{}.offset".format(node)), "filename": cmds.getAttr("{}.filename".format(node)) } audio_data = [] if audio_name: audio_data.append(get_audio_node_data(audio_name)) elif display_sounds: start_frame = int(cmds.playbackOptions(query=True, min=True)) end_frame = int(cmds.playbackOptions(query=True, max=True)) for node in cmds.ls(type="audio"): # Check if frame range and audio range intersections, # for whether to include this audio node or not. duration = cmds.getAttr("{}.duration".format(node)) start_audio = cmds.getAttr("{}.offset".format(node)) end_audio = start_audio + duration if start_audio <= end_frame and end_audio > start_frame: audio_data.append(get_audio_node_data(node)) instance.data["audio"] = audio_data ================================================ FILE: openpype/hosts/maya/plugins/publish/collect_rig_sets.py ================================================ import pyblish.api from maya import cmds class CollectRigSets(pyblish.api.InstancePlugin): """Ensure rig contains pipeline-critical content Every rig must contain at least two object sets: "controls_SET" - Set of all animatable controls "out_SET" - Set of all cacheable meshes """ order = pyblish.api.CollectorOrder + 0.05 label = "Collect Rig Sets" hosts = ["maya"] families = ["rig"] accepted_output = ["mesh", "transform"] accepted_controllers = ["transform"] def process(self, instance): # Find required sets by suffix searching = {"controls_SET", "out_SET", "skeletonAnim_SET", "skeletonMesh_SET"} found = {} for node in cmds.ls(instance, exactType="objectSet"): for suffix in searching: if node.endswith(suffix): found[suffix] = node searching.remove(suffix) break if not searching: break self.log.debug("Found sets: {}".format(found)) rig_sets = instance.data.setdefault("rig_sets", {}) for name, objset in found.items(): rig_sets[name] = objset ================================================ FILE: openpype/hosts/maya/plugins/publish/collect_skeleton_mesh.py ================================================ # -*- coding: utf-8 -*- from maya import cmds # noqa import pyblish.api class CollectSkeletonMesh(pyblish.api.InstancePlugin): """Collect Static Rig Data for FBX Extractor.""" order = pyblish.api.CollectorOrder + 0.2 label = "Collect Skeleton Mesh" hosts = ["maya"] families = ["rig"] def process(self, instance): skeleton_mesh_set = instance.data["rig_sets"].get( "skeletonMesh_SET") if not skeleton_mesh_set: self.log.debug( "No skeletonMesh_SET found. " "Skipping collecting of skeleton mesh..." ) return # Store current frame to ensure single frame export frame = cmds.currentTime(query=True) instance.data["frameStart"] = frame instance.data["frameEnd"] = frame instance.data["skeleton_mesh"] = [] skeleton_mesh_content = cmds.sets( skeleton_mesh_set, query=True) or [] if not skeleton_mesh_content: self.log.debug( "No object nodes in skeletonMesh_SET. " "Skipping collecting of skeleton mesh..." ) return instance.data["families"] += ["rig.fbx"] instance.data["skeleton_mesh"] = skeleton_mesh_content self.log.debug( "Collected skeletonMesh_SET members: {}".format( skeleton_mesh_content )) ================================================ FILE: openpype/hosts/maya/plugins/publish/collect_unreal_skeletalmesh.py ================================================ # -*- coding: utf-8 -*- from maya import cmds # noqa import pyblish.api class CollectUnrealSkeletalMesh(pyblish.api.InstancePlugin): """Collect Unreal Skeletal Mesh.""" order = pyblish.api.CollectorOrder + 0.2 label = "Collect Unreal Skeletal Meshes" families = ["skeletalMesh"] def process(self, instance): frame = cmds.currentTime(query=True) instance.data["frameStart"] = frame instance.data["frameEnd"] = frame geo_sets = [ i for i in instance[:] if i.lower().startswith("geometry_set") ] joint_sets = [ i for i in instance[:] if i.lower().startswith("joints_set") ] instance.data["geometry"] = [] instance.data["joints"] = [] for geo_set in geo_sets: geo_content = cmds.ls(cmds.sets(geo_set, query=True), long=True) if geo_content: instance.data["geometry"] += geo_content for join_set in joint_sets: join_content = cmds.ls(cmds.sets(join_set, query=True), long=True) if join_content: instance.data["joints"] += join_content ================================================ FILE: openpype/hosts/maya/plugins/publish/collect_unreal_staticmesh.py ================================================ # -*- coding: utf-8 -*- from maya import cmds # noqa import pyblish.api from pprint import pformat class CollectUnrealStaticMesh(pyblish.api.InstancePlugin): """Collect Unreal Static Mesh.""" order = pyblish.api.CollectorOrder + 0.2 label = "Collect Unreal Static Meshes" families = ["staticMesh"] def process(self, instance): geometry_set = [ i for i in instance if i.startswith("geometry_SET") ] instance.data["geometryMembers"] = cmds.sets( geometry_set, query=True) self.log.debug("geometry: {}".format( pformat(instance.data.get("geometryMembers")))) collision_set = [ i for i in instance if i.startswith("collisions_SET") ] instance.data["collisionMembers"] = cmds.sets( collision_set, query=True) self.log.debug("collisions: {}".format( pformat(instance.data.get("collisionMembers")))) frame = cmds.currentTime(query=True) instance.data["frameStart"] = frame instance.data["frameEnd"] = frame ================================================ FILE: openpype/hosts/maya/plugins/publish/collect_user_defined_attributes.py ================================================ from maya import cmds import pyblish.api class CollectUserDefinedAttributes(pyblish.api.InstancePlugin): """Collect user defined attributes for nodes in instance.""" order = pyblish.api.CollectorOrder + 0.45 families = ["pointcache", "animation", "usd"] label = "Collect User Defined Attributes" hosts = ["maya"] def process(self, instance): # Collect user defined attributes. if not instance.data.get("includeUserDefinedAttributes", False): return if "out_hierarchy" in instance.data: # animation family nodes = instance.data["out_hierarchy"] else: nodes = instance[:] if not nodes: return shapes = cmds.listRelatives(nodes, shapes=True, fullPath=True) or [] nodes = set(nodes).union(shapes) attrs = cmds.listAttr(list(nodes), userDefined=True) or [] user_defined_attributes = list(sorted(set(attrs))) instance.data["userDefinedAttributes"] = user_defined_attributes self.log.debug( "Collected user defined attributes: {}".format( ", ".join(user_defined_attributes) ) ) ================================================ FILE: openpype/hosts/maya/plugins/publish/collect_vrayproxy.py ================================================ # -*- coding: utf-8 -*- """Collect Vray Proxy.""" import pyblish.api class CollectVrayProxy(pyblish.api.InstancePlugin): """Collect Vray Proxy instance. Add `pointcache` family for it. """ order = pyblish.api.CollectorOrder + 0.01 label = "Collect Vray Proxy" families = ["vrayproxy"] def process(self, instance): """Collector entry point.""" if not instance.data.get('families'): instance.data["families"] = [] if instance.data.get("vrmesh"): instance.data["families"].append("vrayproxy.vrmesh") if instance.data.get("alembic"): instance.data["families"].append("vrayproxy.alembic") ================================================ FILE: openpype/hosts/maya/plugins/publish/collect_vrayscene.py ================================================ # -*- coding: utf-8 -*- """Collect Vray Scene and prepare it for extraction and publishing.""" import re import maya.app.renderSetup.model.renderSetup as renderSetup from maya import cmds import pyblish.api from openpype.pipeline import legacy_io from openpype.lib import get_formatted_current_time from openpype.hosts.maya.api import lib class CollectVrayScene(pyblish.api.InstancePlugin): """Collect Vray Scene. If export on farm is checked, job is created to export it. """ order = pyblish.api.CollectorOrder + 0.01 label = "Collect Vray Scene" families = ["vrayscene"] def process(self, instance): """Collector entry point.""" context = instance.context layer = instance.data["transientData"]["layer"] layer_name = layer.name() renderer = self.get_render_attribute("currentRenderer", layer=layer_name) if renderer != "vray": self.log.warning("Layer '{}' renderer is not set to V-Ray".format( layer_name )) # collect all frames we are expecting to be rendered frame_start_render = int(self.get_render_attribute( "startFrame", layer=layer_name)) frame_end_render = int(self.get_render_attribute( "endFrame", layer=layer_name)) if (int(context.data['frameStartHandle']) == frame_start_render and int(context.data['frameEndHandle']) == frame_end_render): # noqa: W503, E501 handle_start = context.data['handleStart'] handle_end = context.data['handleEnd'] frame_start = context.data['frameStart'] frame_end = context.data['frameEnd'] frame_start_handle = context.data['frameStartHandle'] frame_end_handle = context.data['frameEndHandle'] else: handle_start = 0 handle_end = 0 frame_start = frame_start_render frame_end = frame_end_render frame_start_handle = frame_start_render frame_end_handle = frame_end_render # Get layer specific settings, might be overrides data = { "subset": layer_name, "layer": layer_name, # TODO: This likely needs fixing now # Before refactor: cmds.sets(layer, q=True) or ["*"] "setMembers": ["*"], "review": False, "publish": True, "handleStart": handle_start, "handleEnd": handle_end, "frameStart": frame_start, "frameEnd": frame_end, "frameStartHandle": frame_start_handle, "frameEndHandle": frame_end_handle, "byFrameStep": int( self.get_render_attribute("byFrameStep", layer=layer_name)), "renderer": renderer, # instance subset "family": "vrayscene_layer", "families": ["vrayscene_layer"], "time": get_formatted_current_time(), "author": context.data["user"], # Add source to allow tracing back to the scene from # which was submitted originally "source": context.data["currentFile"].replace("\\", "/"), "resolutionWidth": lib.get_attr_in_layer( "defaultResolution.height", layer=layer_name ), "resolutionHeight": lib.get_attr_in_layer( "defaultResolution.width", layer=layer_name ), "pixelAspect": lib.get_attr_in_layer( "defaultResolution.pixelAspect", layer=layer_name ), "priority": instance.data.get("priority"), "useMultipleSceneFiles": instance.data.get( "vraySceneMultipleFiles") } instance.data.update(data) # Define nice label label = "{0} ({1})".format(layer_name, instance.data["asset"]) label += " [{0}-{1}]".format( int(data["frameStartHandle"]), int(data["frameEndHandle"]) ) instance.data["label"] = label def get_render_attribute(self, attr, layer): """Get attribute from render options. Args: attr (str): name of attribute to be looked up. Returns: Attribute value """ return lib.get_attr_in_layer( "defaultRenderGlobals.{}".format(attr), layer=layer ) ================================================ FILE: openpype/hosts/maya/plugins/publish/collect_workfile.py ================================================ import os import pyblish.api class CollectWorkfileData(pyblish.api.InstancePlugin): """Inject data into Workfile instance""" order = pyblish.api.CollectorOrder - 0.01 label = "Maya Workfile" hosts = ['maya'] families = ["workfile"] def process(self, instance): """Inject the current working file""" context = instance.context current_file = instance.context.data['currentFile'] folder, file = os.path.split(current_file) filename, ext = os.path.splitext(file) data = { # noqa "setMembers": [current_file], "frameStart": context.data['frameStart'], "frameEnd": context.data['frameEnd'], "handleStart": context.data['handleStart'], "handleEnd": context.data['handleEnd'] } data['representations'] = [{ 'name': ext.lstrip("."), 'ext': ext.lstrip("."), 'files': file, "stagingDir": folder, }] instance.data.update(data) ================================================ FILE: openpype/hosts/maya/plugins/publish/collect_workscene_fps.py ================================================ import pyblish.api from maya import mel class CollectWorksceneFPS(pyblish.api.ContextPlugin): """Get the FPS of the work scene""" label = "Workscene FPS" order = pyblish.api.CollectorOrder hosts = ["maya"] def process(self, context): fps = mel.eval('currentTimeUnitToFPS()') self.log.info("Workscene FPS: %s" % fps) context.data.update({"fps": fps}) ================================================ FILE: openpype/hosts/maya/plugins/publish/collect_xgen.py ================================================ import os from maya import cmds import pyblish.api from openpype.hosts.maya.api.lib import get_attribute_input class CollectXgen(pyblish.api.InstancePlugin): """Collect Xgen""" order = pyblish.api.CollectorOrder + 0.499999 label = "Collect Xgen" families = ["xgen"] def process(self, instance): data = { "xgmPalettes": cmds.ls(instance, type="xgmPalette", long=True), "xgmDescriptions": cmds.ls( instance, type="xgmDescription", long=True ), "xgmSubdPatches": cmds.ls(instance, type="xgmSubdPatch", long=True) } data["xgenNodes"] = ( data["xgmPalettes"] + data["xgmDescriptions"] + data["xgmSubdPatches"] ) if data["xgmPalettes"]: data["xgmPalette"] = data["xgmPalettes"][0] data["xgenConnections"] = set() for node in data["xgmSubdPatches"]: connected_transform = get_attribute_input( node + ".transform" ).split(".")[0] data["xgenConnections"].add(connected_transform) # Collect all files under palette root as resources. import xgenm data_path = xgenm.getAttr( "xgDataPath", data["xgmPalette"].replace("|", "") ).split(os.pathsep)[0] data_path = data_path.replace( "${PROJECT}", xgenm.getAttr("xgProjectPath", data["xgmPalette"].replace("|", "")) ) transfers = [] # Since we are duplicating this palette when extracting we predict that # the name will be the basename without namespaces. predicted_palette_name = data["xgmPalette"].split(":")[-1] predicted_palette_name = predicted_palette_name.replace("|", "") for root, _, files in os.walk(data_path): for file in files: source = os.path.join(root, file).replace("\\", "/") destination = os.path.join( instance.data["resourcesDir"], "collections", predicted_palette_name, source.replace(data_path, "")[1:] ) transfers.append((source, destination.replace("\\", "/"))) data["transfers"] = transfers self.log.debug(data) instance.data.update(data) ================================================ FILE: openpype/hosts/maya/plugins/publish/collect_yeti_cache.py ================================================ from maya import cmds import pyblish.api from openpype.hosts.maya.api import lib SETTINGS = { # Preview "displayOutput", "colorR", "colorG", "colorB", "viewportDensity", "viewportWidth", "viewportLength", # Render attributes "renderDensity", "renderWidth", "renderLength", "increaseRenderBounds", "imageSearchPath", # Pipeline specific "cbId" } class CollectYetiCache(pyblish.api.InstancePlugin): """Collect all information of the Yeti caches The information contains the following attributes per Yeti node - "renderDensity" - "renderWidth" - "renderLength" - "increaseRenderBounds" - "imageSearchPath" Other information is the name of the transform and it's Colorbleed ID """ order = pyblish.api.CollectorOrder + 0.45 label = "Collect Yeti Cache" families = ["yetiRig", "yeticache", "yeticacheUE"] hosts = ["maya"] def process(self, instance): # Collect fur settings settings = {"nodes": []} # Get yeti nodes and their transforms yeti_shapes = cmds.ls(instance, type="pgYetiMaya") for shape in yeti_shapes: # Get specific node attributes attr_data = {} for attr in SETTINGS: current = cmds.getAttr("%s.%s" % (shape, attr)) # change None to empty string as Maya doesn't support # NoneType in attributes if current is None: current = "" attr_data[attr] = current # Get transform data parent = cmds.listRelatives(shape, parent=True)[0] transform_data = {"name": parent, "cbId": lib.get_id(parent)} shape_data = { "transform": transform_data, "name": shape, "cbId": lib.get_id(shape), "attrs": attr_data, } settings["nodes"].append(shape_data) instance.data["fursettings"] = settings ================================================ FILE: openpype/hosts/maya/plugins/publish/collect_yeti_rig.py ================================================ import os import re from maya import cmds import pyblish.api from openpype.hosts.maya.api import lib from openpype.pipeline.publish import KnownPublishError SETTINGS = {"renderDensity", "renderWidth", "renderLength", "increaseRenderBounds", "imageSearchPath", "cbId"} class CollectYetiRig(pyblish.api.InstancePlugin): """Collect all information of the Yeti Rig""" order = pyblish.api.CollectorOrder + 0.4 label = "Collect Yeti Rig" families = ["yetiRig"] hosts = ["maya"] def process(self, instance): assert "input_SET" in instance.data["setMembers"], ( "Yeti Rig must have an input_SET") input_connections = self.collect_input_connections(instance) # Collect any textures if used yeti_resources = [] yeti_nodes = cmds.ls(instance[:], type="pgYetiMaya", long=True) for node in yeti_nodes: # Get Yeti resources (textures) resources = self.get_yeti_resources(node) yeti_resources.extend(resources) instance.data["rigsettings"] = {"inputs": input_connections} instance.data["resources"] = yeti_resources # Force frame range for yeti cache export for the rig start = cmds.playbackOptions(query=True, animationStartTime=True) for key in ["frameStart", "frameEnd", "frameStartHandle", "frameEndHandle"]: instance.data[key] = start instance.data["preroll"] = 0 def collect_input_connections(self, instance): """Collect the inputs for all nodes in the input_SET""" # Get the input meshes information input_content = cmds.ls(cmds.sets("input_SET", query=True), long=True) # Include children input_content += cmds.listRelatives(input_content, allDescendents=True, fullPath=True) or [] # Ignore intermediate objects input_content = cmds.ls(input_content, long=True, noIntermediate=True) if not input_content: return [] # Store all connections connections = cmds.listConnections(input_content, source=True, destination=False, connections=True, # Only allow inputs from dagNodes # (avoid display layers, etc.) type="dagNode", plugs=True) or [] connections = cmds.ls(connections, long=True) # Ensure long names inputs = [] for dest, src in lib.pairwise(connections): source_node, source_attr = src.split(".", 1) dest_node, dest_attr = dest.split(".", 1) # Ensure the source of the connection is not included in the # current instance's hierarchy. If so, we ignore that connection # as we will want to preserve it even over a publish. if source_node in instance: self.log.debug("Ignoring input connection between nodes " "inside the instance: %s -> %s" % (src, dest)) continue inputs.append({"connections": [source_attr, dest_attr], "sourceID": lib.get_id(source_node), "destinationID": lib.get_id(dest_node)}) return inputs def get_yeti_resources(self, node): """Get all resource file paths If a texture is a sequence it gathers all sibling files to ensure the texture sequence is complete. References can be used in the Yeti graph, this means that it is possible to load previously caches files. The information will need to be stored and, if the file not publish, copied to the resource folder. Args: node (str): node name of the pgYetiMaya node Returns: list """ resources = [] image_search_paths = cmds.getAttr("{}.imageSearchPath".format(node)) if image_search_paths: # TODO: Somehow this uses OS environment path separator, `:` vs `;` # Later on check whether this is pipeline OS cross-compatible. image_search_paths = [p for p in image_search_paths.split(os.path.pathsep) if p] # find all ${TOKEN} tokens and replace them with $TOKEN env. variable image_search_paths = self._replace_tokens(image_search_paths) # List all related textures texture_nodes = cmds.pgYetiGraph( node, listNodes=True, type="texture") texture_filenames = [ cmds.pgYetiGraph( node, node=texture_node, param="file_name", getParamValue=True) for texture_node in texture_nodes ] self.log.debug("Found %i texture(s)" % len(texture_filenames)) # Get all reference nodes reference_nodes = cmds.pgYetiGraph(node, listNodes=True, type="reference") self.log.debug("Found %i reference node(s)" % len(reference_nodes)) # Collect all texture files # find all ${TOKEN} tokens and replace them with $TOKEN env. variable texture_filenames = self._replace_tokens(texture_filenames) for texture in texture_filenames: files = [] if os.path.isabs(texture): self.log.debug("Texture is absolute path, ignoring " "image search paths for: %s" % texture) files = self.search_textures(texture) else: for root in image_search_paths: filepath = os.path.join(root, texture) files = self.search_textures(filepath) if files: # Break out on first match in search paths.. break if not files: raise KnownPublishError( "No texture found for: %s " "(searched: %s)" % (texture, image_search_paths)) item = { "files": files, "source": texture, "node": node } resources.append(item) # For now validate that every texture has at least a single file # resolved. Since a 'resource' does not have the requirement of having # a `files` explicitly mapped it's not explicitly validated. # TODO: Validate this as a validator invalid_resources = [] for resource in resources: if not resource['files']: invalid_resources.append(resource) if invalid_resources: raise RuntimeError("Invalid resources") # Collect all referenced files for reference_node in reference_nodes: ref_file = cmds.pgYetiGraph(node, node=reference_node, param="reference_file", getParamValue=True) # Create resource dict item = { "source": ref_file, "node": node, "graphnode": reference_node, "param": "reference_file", "files": [] } ref_file_name = os.path.basename(ref_file) if "%04d" in ref_file_name: item["files"] = self.get_sequence(ref_file) else: if os.path.exists(ref_file) and os.path.isfile(ref_file): item["files"] = [ref_file] if not item["files"]: self.log.warning("Reference node '%s' has no valid file " "path set: %s" % (reference_node, ref_file)) # TODO: This should allow to pass and fail in Validator instead raise RuntimeError("Reference node must be a full file path!") resources.append(item) return resources def search_textures(self, filepath): """Search all texture files on disk. This also parses to full sequences for those with dynamic patterns like and %04d in the filename. Args: filepath (str): The full path to the file, including any dynamic patterns like or %04d Returns: list: The files found on disk """ filename = os.path.basename(filepath) # Collect full sequence if it matches a sequence pattern if len(filename.split(".")) > 2: # For UDIM based textures (tiles) if " " in filename: sequences = self.get_sequence(filepath, pattern=" ") if sequences: return sequences # Frame/time - Based textures (animated masks f.e) elif "%04d" in filename: sequences = self.get_sequence(filepath, pattern="%04d") if sequences: return sequences # Assuming it is a fixed name (single file) if os.path.exists(filepath): return [filepath] return [] def get_sequence(self, filepath, pattern="%04d"): """Get sequence from filename. This will only return files if they exist on disk as it tries to collect the sequence using the filename pattern and searching for them on disk. Supports negative frame ranges like -001, 0000, 0001 and -0001, 0000, 0001. Arguments: filepath (str): The full path to filename containing the given pattern. pattern (str): The pattern to swap with the variable frame number. Returns: list: file sequence. """ import clique escaped = re.escape(filepath) re_pattern = escaped.replace(pattern, "-?[0-9]+") source_dir = os.path.dirname(filepath) files = [f for f in os.listdir(source_dir) if re.match(re_pattern, f)] pattern = [clique.PATTERNS["frames"]] collection, remainder = clique.assemble(files, patterns=pattern) return collection def _replace_tokens(self, strings): env_re = re.compile(r"\$\{(\w+)\}") replaced = [] for s in strings: matches = re.finditer(env_re, s) for m in matches: try: s = s.replace(m.group(), os.environ[m.group(1)]) except KeyError: msg = "Cannot find requested {} in environment".format( m.group(1)) self.log.error(msg) raise RuntimeError(msg) replaced.append(s) return replaced ================================================ FILE: openpype/hosts/maya/plugins/publish/determine_future_version.py ================================================ import pyblish class DetermineFutureVersion(pyblish.api.InstancePlugin): """ This will determine version of subset if we want render to be attached to. """ label = "Determine Subset Version" order = pyblish.api.IntegratorOrder hosts = ["maya"] families = ["renderlayer"] def process(self, instance): context = instance.context attach_to_subsets = [s["subset"] for s in instance.data['attachTo']] if not attach_to_subsets: return for i in context: if i.data["subset"] in attach_to_subsets: # # this will get corresponding subset in attachTo list # # so we can set version there sub = next(item for item in instance.data['attachTo'] if item["subset"] == i.data["subset"]) # noqa: E501 sub["version"] = i.data.get("version", 1) self.log.info("render will be attached to {} v{}".format( sub["subset"], sub["version"] )) ================================================ FILE: openpype/hosts/maya/plugins/publish/extract_active_view_thumbnail.py ================================================ import maya.api.OpenMaya as om import maya.api.OpenMayaUI as omui import pyblish.api import tempfile from openpype.hosts.maya.api.lib import IS_HEADLESS class ExtractActiveViewThumbnail(pyblish.api.InstancePlugin): """Set instance thumbnail to a screengrab of current active viewport. This makes it so that if an instance does not have a thumbnail set yet that it will get a thumbnail of the currently active view at the time of publishing as a fallback. """ order = pyblish.api.ExtractorOrder + 0.49 label = "Active View Thumbnail" families = ["workfile"] hosts = ["maya"] def process(self, instance): if IS_HEADLESS: self.log.debug( "Skip extraction of active view thumbnail, due to being in" "headless mode." ) return thumbnail = instance.data.get("thumbnailPath") if not thumbnail: view_thumbnail = self.get_view_thumbnail(instance) if not view_thumbnail: return self.log.debug("Setting instance thumbnail path to: {}".format( view_thumbnail )) instance.data["thumbnailPath"] = view_thumbnail def get_view_thumbnail(self, instance): cache_key = "__maya_view_thumbnail" context = instance.context if cache_key not in context.data: # Generate only a single thumbnail, even for multiple instances with tempfile.NamedTemporaryFile(suffix="_thumbnail.jpg", delete=False) as f: path = f.name view = omui.M3dView.active3dView() image = om.MImage() view.readColorBuffer(image, True) image.writeToFile(path, "jpg") self.log.debug("Generated thumbnail: {}".format(path)) context.data["cleanupFullPaths"].append(path) context.data[cache_key] = path return context.data[cache_key] ================================================ FILE: openpype/hosts/maya/plugins/publish/extract_arnold_scene_source.py ================================================ import os from collections import defaultdict import json from maya import cmds import arnold from openpype.pipeline import publish from openpype.hosts.maya.api import lib class ExtractArnoldSceneSource(publish.Extractor): """Extract the content of the instance to an Arnold Scene Source file.""" label = "Extract Arnold Scene Source" hosts = ["maya"] families = ["ass"] asciiAss = True def _pre_process(self, instance, staging_dir): file_path = os.path.join(staging_dir, "{}.ass".format(instance.name)) # Mask mask = arnold.AI_NODE_ALL node_types = { "options": arnold.AI_NODE_OPTIONS, "camera": arnold.AI_NODE_CAMERA, "light": arnold.AI_NODE_LIGHT, "shape": arnold.AI_NODE_SHAPE, "shader": arnold.AI_NODE_SHADER, "override": arnold.AI_NODE_OVERRIDE, "driver": arnold.AI_NODE_DRIVER, "filter": arnold.AI_NODE_FILTER, "color_manager": arnold.AI_NODE_COLOR_MANAGER, "operator": arnold.AI_NODE_OPERATOR } for key in node_types.keys(): if instance.data.get("mask" + key.title()): mask = mask ^ node_types[key] # Motion blur attribute_data = { "defaultArnoldRenderOptions.motion_blur_enable": instance.data.get( "motionBlur", True ), "defaultArnoldRenderOptions.motion_steps": instance.data.get( "motionBlurKeys", 2 ), "defaultArnoldRenderOptions.motion_frames": instance.data.get( "motionBlurLength", 0.5 ) } # Write out .ass file kwargs = { "filename": file_path, "startFrame": instance.data.get("frameStartHandle", 1), "endFrame": instance.data.get("frameEndHandle", 1), "frameStep": instance.data.get("step", 1), "selected": True, "asciiAss": self.asciiAss, "shadowLinks": True, "lightLinks": True, "boundingBox": True, "expandProcedurals": instance.data.get("expandProcedurals", False), "camera": instance.data["camera"], "mask": mask } if "representations" not in instance.data: instance.data["representations"] = [] return attribute_data, kwargs def process(self, instance): staging_dir = self.staging_dir(instance) attribute_data, kwargs = self._pre_process(instance, staging_dir) filenames = self._extract( instance.data["members"], attribute_data, kwargs ) self._post_process( instance, filenames, staging_dir, kwargs["startFrame"] ) def _post_process(self, instance, filenames, staging_dir, frame_start): nodes_by_id = self._nodes_by_id(instance[:]) representation = { "name": "ass", "ext": "ass", "files": filenames if len(filenames) > 1 else filenames[0], "stagingDir": staging_dir, "frameStart": frame_start } instance.data["representations"].append(representation) json_path = os.path.join( staging_dir, "{}.json".format(instance.name) ) with open(json_path, "w") as f: json.dump(nodes_by_id, f) representation = { "name": "json", "ext": "json", "files": os.path.basename(json_path), "stagingDir": staging_dir } instance.data["representations"].append(representation) self.log.debug( "Extracted instance {} to: {}".format(instance.name, staging_dir) ) def _nodes_by_id(self, nodes): nodes_by_id = defaultdict(list) for node in nodes: id = lib.get_id(node) if id is None: continue # Converting Maya hierarchy separator "|" to Arnold separator "/". nodes_by_id[id].append(node.replace("|", "/")) return nodes_by_id def _extract(self, nodes, attribute_data, kwargs): filenames = [] with lib.attribute_values(attribute_data): with lib.maintained_selection(): self.log.debug( "Writing: {}".format(nodes) ) cmds.select(nodes, noExpand=True) self.log.debug( "Extracting ass sequence with: {}".format(kwargs) ) exported_files = cmds.arnoldExportAss(**kwargs) for file in exported_files: filenames.append(os.path.split(file)[1]) self.log.debug("Exported: {}".format(filenames)) return filenames class ExtractArnoldSceneSourceProxy(ExtractArnoldSceneSource): """Extract the content of the instance to an Arnold Scene Source file.""" label = "Extract Arnold Scene Source Proxy" hosts = ["maya"] families = ["assProxy"] asciiAss = True def process(self, instance): staging_dir = self.staging_dir(instance) attribute_data, kwargs = self._pre_process(instance, staging_dir) filenames, _ = self._duplicate_extract( instance.data["members"], attribute_data, kwargs ) self._post_process( instance, filenames, staging_dir, kwargs["startFrame"] ) kwargs["filename"] = os.path.join( staging_dir, "{}_proxy.ass".format(instance.name) ) filenames, _ = self._duplicate_extract( instance.data["proxy"], attribute_data, kwargs ) representation = { "name": "proxy", "ext": "ass", "files": filenames if len(filenames) > 1 else filenames[0], "stagingDir": staging_dir, "frameStart": kwargs["startFrame"], "outputName": "proxy" } instance.data["representations"].append(representation) def _duplicate_extract(self, nodes, attribute_data, kwargs): self.log.debug( "Writing {} with:\n{}".format(kwargs["filename"], kwargs) ) filenames = [] # Duplicating nodes so they are direct children of the world. This # makes the hierarchy of any exported ass file the same. with lib.delete_after() as delete_bin: duplicate_nodes = [] for node in nodes: # Only interested in transforms: if cmds.nodeType(node) != "transform": continue # Only interested in transforms with shapes. shapes = cmds.listRelatives( node, shapes=True, noIntermediate=True ) if not shapes: continue basename = cmds.duplicate(node)[0] parents = cmds.ls(node, long=True)[0].split("|")[:-1] duplicate_transform = "|".join(parents + [basename]) if cmds.listRelatives(duplicate_transform, parent=True): duplicate_transform = cmds.parent( duplicate_transform, world=True )[0] basename = node.rsplit("|", 1)[-1].rsplit(":", 1)[-1] duplicate_transform = cmds.rename( duplicate_transform, basename ) # Discard children nodes that are not shapes shapes = cmds.listRelatives( duplicate_transform, shapes=True, fullPath=True ) children = cmds.listRelatives( duplicate_transform, children=True, fullPath=True ) cmds.delete(set(children) - set(shapes)) duplicate_nodes.append(duplicate_transform) duplicate_nodes.extend(shapes) delete_bin.append(duplicate_transform) nodes_by_id = self._nodes_by_id(duplicate_nodes) filenames = self._extract(duplicate_nodes, attribute_data, kwargs) return filenames, nodes_by_id ================================================ FILE: openpype/hosts/maya/plugins/publish/extract_assembly.py ================================================ import os import json from openpype.pipeline import publish from openpype.hosts.maya.api.lib import extract_alembic from maya import cmds class ExtractAssembly(publish.Extractor): """Produce an alembic of just point positions and normals. Positions and normals are preserved, but nothing more, for plain and predictable point caches. """ label = "Extract Assembly" hosts = ["maya"] families = ["assembly"] def process(self, instance): staging_dir = self.staging_dir(instance) hierarchy_filename = "{}.abc".format(instance.name) hierarchy_path = os.path.join(staging_dir, hierarchy_filename) json_filename = "{}.json".format(instance.name) json_path = os.path.join(staging_dir, json_filename) self.log.debug("Dumping scene data for debugging ..") with open(json_path, "w") as filepath: json.dump(instance.data["scenedata"], filepath, ensure_ascii=False) self.log.debug("Extracting pointcache ..") cmds.select(instance.data["nodesHierarchy"]) # Run basic alembic exporter extract_alembic(file=hierarchy_path, startFrame=1.0, endFrame=1.0, **{"step": 1.0, "attr": ["cbId"], "writeVisibility": True, "writeCreases": True, "uvWrite": True, "selection": True}) if "representations" not in instance.data: instance.data["representations"] = [] representation_abc = { 'name': 'abc', 'ext': 'abc', 'files': hierarchy_filename, "stagingDir": staging_dir } instance.data["representations"].append(representation_abc) representation_json = { 'name': 'json', 'ext': 'json', 'files': json_filename, "stagingDir": staging_dir } instance.data["representations"].append(representation_json) # Remove data instance.data.pop("scenedata", None) cmds.select(clear=True) ================================================ FILE: openpype/hosts/maya/plugins/publish/extract_camera_alembic.py ================================================ import os from maya import cmds from openpype.pipeline import publish from openpype.hosts.maya.api import lib class ExtractCameraAlembic(publish.Extractor, publish.OptionalPyblishPluginMixin): """Extract a Camera as Alembic. The camera gets baked to world space by default. Only when the instance's `bakeToWorldSpace` is set to False it will include its full hierarchy. 'camera' family expects only single camera, if multiple cameras are needed, 'matchmove' is better choice. """ label = "Extract Camera (Alembic)" hosts = ["maya"] families = ["camera", "matchmove"] bake_attributes = [] def process(self, instance): # Collect the start and end including handles start = instance.data["frameStartHandle"] end = instance.data["frameEndHandle"] step = instance.data.get("step", 1.0) bake_to_worldspace = instance.data("bakeToWorldSpace", True) # get cameras members = instance.data['setMembers'] cameras = cmds.ls(members, leaf=True, long=True, dag=True, type="camera") # validate required settings assert isinstance(step, float), "Step must be a float value" # Define extract output file path dir_path = self.staging_dir(instance) if not os.path.exists(dir_path): os.makedirs(dir_path) filename = "{0}.abc".format(instance.name) path = os.path.join(dir_path, filename) # Perform alembic extraction member_shapes = cmds.ls( members, leaf=True, shapes=True, long=True, dag=True) with lib.maintained_selection(): cmds.select( member_shapes, replace=True, noExpand=True) # Enforce forward slashes for AbcExport because we're # embedding it into a job string path = path.replace("\\", "/") job_str = ' -selection -dataFormat "ogawa" ' job_str += ' -attrPrefix cb' job_str += ' -frameRange {0} {1} '.format(start, end) job_str += ' -step {0} '.format(step) if bake_to_worldspace: job_str += ' -worldSpace' # if baked, drop the camera hierarchy to maintain # clean output and backwards compatibility camera_roots = cmds.listRelatives( cameras, parent=True, fullPath=True) for camera_root in camera_roots: job_str += ' -root {0}'.format(camera_root) for member in members: descendants = cmds.listRelatives(member, allDescendents=True, fullPath=True) or [] shapes = cmds.ls(descendants, shapes=True, noIntermediate=True, long=True) cameras = cmds.ls(shapes, type="camera", long=True) if cameras: if not set(shapes) - set(cameras): continue self.log.warning(( "Camera hierarchy contains additional geometry. " "Extraction will fail.") ) transform = cmds.listRelatives( member, parent=True, fullPath=True) transform = transform[0] if transform else member job_str += ' -root {0}'.format(transform) job_str += ' -file "{0}"'.format(path) # bake specified attributes in preset assert isinstance(self.bake_attributes, (list, tuple)), ( "Attributes to bake must be specified as a list" ) for attr in self.bake_attributes: self.log.debug("Adding {} attribute".format(attr)) job_str += " -attr {0}".format(attr) with lib.evaluation("off"): with lib.suspended_refresh(): cmds.AbcExport(j=job_str, verbose=False) if "representations" not in instance.data: instance.data["representations"] = [] representation = { 'name': 'abc', 'ext': 'abc', 'files': filename, "stagingDir": dir_path, } instance.data["representations"].append(representation) self.log.debug("Extracted instance '{0}' to: {1}".format( instance.name, path)) ================================================ FILE: openpype/hosts/maya/plugins/publish/extract_camera_mayaScene.py ================================================ # -*- coding: utf-8 -*- """Extract camera as Maya Scene.""" import os import itertools import contextlib from maya import cmds from openpype.pipeline import publish from openpype.hosts.maya.api import lib from openpype.lib import ( BoolDef ) def massage_ma_file(path): """Clean up .ma file for backwards compatibility. Massage the .ma of baked camera to stay backwards compatible with older versions of Fusion (6.4) """ # Get open file's lines f = open(path, "r+") lines = f.readlines() f.seek(0) # reset to start of file # Rewrite the file for line in lines: # Skip all 'rename -uid' lines stripped = line.strip() if stripped.startswith("rename -uid "): continue f.write(line) f.truncate() # remove remainder f.close() def grouper(iterable, n, fillvalue=None): """Collect data into fixed-length chunks or blocks. Examples: grouper('ABCDEFG', 3, 'x') --> ABC DEF Gxx """ args = [iter(iterable)] * n from six.moves import zip_longest return zip_longest(fillvalue=fillvalue, *args) def unlock(plug): """Unlocks attribute and disconnects inputs for a plug. This will also recursively unlock the attribute upwards to any parent attributes for compound attributes, to ensure it's fully unlocked and free to change the value. """ node, attr = plug.rsplit(".", 1) # Unlock attribute cmds.setAttr(plug, lock=False) # Also unlock any parent attribute (if compound) parents = cmds.attributeQuery(attr, node=node, listParent=True) if parents: for parent in parents: unlock("{0}.{1}".format(node, parent)) # Break incoming connections connections = cmds.listConnections(plug, source=True, destination=False, plugs=True, connections=True) if connections: for destination, source in grouper(connections, 2): cmds.disconnectAttr(source, destination) class ExtractCameraMayaScene(publish.Extractor, publish.OptionalPyblishPluginMixin): """Extract a Camera as Maya Scene. This will create a duplicate of the camera that will be baked *with* substeps and handles for the required frames. This temporary duplicate will be published. The cameras gets baked to world space by default. Only when the instance's `bakeToWorldSpace` is set to False it will include its full hierarchy. 'camera' family expects only single camera, if multiple cameras are needed, 'matchmove' is better choice. Note: The extracted Maya ascii file gets "massaged" removing the uuid values so they are valid for older versions of Fusion (e.g. 6.4) """ label = "Extract Camera (Maya Scene)" hosts = ["maya"] families = ["camera", "matchmove"] scene_type = "ma" keep_image_planes = True def process(self, instance): """Plugin entry point.""" # get settings ext_mapping = ( instance.context.data["project_settings"]["maya"]["ext_mapping"] ) if ext_mapping: self.log.debug("Looking in settings for scene type ...") # use extension mapping for first family found for family in self.families: try: self.scene_type = ext_mapping[family] self.log.debug( "Using {} as scene type".format(self.scene_type)) break except KeyError: # no preset found pass # Collect the start and end including handles start = instance.data["frameStartHandle"] end = instance.data["frameEndHandle"] step = instance.data.get("step", 1.0) bake_to_worldspace = instance.data("bakeToWorldSpace", True) if not bake_to_worldspace: self.log.warning("Camera (Maya Scene) export only supports world" "space baked camera extractions. The disabled " "bake to world space is ignored...") # get cameras members = set(cmds.ls(instance.data['setMembers'], leaf=True, shapes=True, long=True, dag=True)) cameras = set(cmds.ls(members, leaf=True, shapes=True, long=True, dag=True, type="camera")) # validate required settings assert isinstance(step, float), "Step must be a float value" transforms = cmds.listRelatives(list(cameras), parent=True, fullPath=True) # Define extract output file path dir_path = self.staging_dir(instance) filename = "{0}.{1}".format(instance.name, self.scene_type) path = os.path.join(dir_path, filename) # Perform extraction with lib.maintained_selection(): with lib.evaluation("off"): with lib.suspended_refresh(): if bake_to_worldspace: baked = lib.bake_to_world_space( transforms, frame_range=[start, end], step=step ) baked_camera_shapes = set(cmds.ls(baked, type="camera", dag=True, shapes=True, long=True)) members.update(baked_camera_shapes) members.difference_update(cameras) else: baked_camera_shapes = cmds.ls(list(cameras), type="camera", dag=True, shapes=True, long=True) attrs = {"backgroundColorR": 0.0, "backgroundColorG": 0.0, "backgroundColorB": 0.0, "overscan": 1.0} # Fix PLN-178: Don't allow background color to be non-black for cam, (attr, value) in itertools.product(cmds.ls( baked_camera_shapes, type="camera", dag=True, long=True), attrs.items()): plug = "{0}.{1}".format(cam, attr) unlock(plug) cmds.setAttr(plug, value) attr_values = self.get_attr_values_from_data( instance.data) keep_image_planes = attr_values.get("keep_image_planes") with transfer_image_planes(sorted(cameras), sorted(baked_camera_shapes), keep_image_planes): self.log.info("Performing extraction..") cmds.select(cmds.ls(list(members), dag=True, shapes=True, long=True), noExpand=True) cmds.file(path, force=True, typ="mayaAscii" if self.scene_type == "ma" else "mayaBinary", # noqa: E501 exportSelected=True, preserveReferences=False, constructionHistory=False, channels=True, # allow animation constraints=False, shader=False, expressions=False) # Delete the baked hierarchy if bake_to_worldspace: cmds.delete(baked) if self.scene_type == "ma": massage_ma_file(path) if "representations" not in instance.data: instance.data["representations"] = [] representation = { 'name': self.scene_type, 'ext': self.scene_type, 'files': filename, "stagingDir": dir_path, } instance.data["representations"].append(representation) self.log.debug("Extracted instance '{0}' to: {1}".format( instance.name, path)) @classmethod def get_attribute_defs(cls): defs = super(ExtractCameraMayaScene, cls).get_attribute_defs() defs.extend([ BoolDef("keep_image_planes", label="Keep Image Planes", tooltip="Preserving connected image planes on camera", default=cls.keep_image_planes), ]) return defs @contextlib.contextmanager def transfer_image_planes(source_cameras, target_cameras, keep_input_connections): """Reattaches image planes to baked or original cameras. Baked cameras are duplicates of original ones. This attaches it to duplicated camera properly and after export it reattaches it back to original to keep image plane in workfile. """ originals = {} try: for source_camera, target_camera in zip(source_cameras, target_cameras): image_plane_plug = "{}.imagePlane".format(source_camera) image_planes = cmds.listConnections(image_plane_plug, source=True, destination=False, type="imagePlane") or [] # Split of the parent path they are attached - we want # the image plane node name if attached to a camera. # TODO: Does this still mean the image plane name is unique? image_planes = [x.split("->", 1)[-1] for x in image_planes] if not image_planes: continue originals[source_camera] = [] for image_plane in image_planes: if keep_input_connections: if source_camera == target_camera: continue _attach_image_plane(target_camera, image_plane) else: # explicitly detach image planes cmds.imagePlane(image_plane, edit=True, detach=True) originals[source_camera].append(image_plane) yield finally: for camera, image_planes in originals.items(): for image_plane in image_planes: _attach_image_plane(camera, image_plane) def _attach_image_plane(camera, image_plane): cmds.imagePlane(image_plane, edit=True, detach=True) cmds.imagePlane(image_plane, edit=True, camera=camera) ================================================ FILE: openpype/hosts/maya/plugins/publish/extract_fbx.py ================================================ # -*- coding: utf-8 -*- import os from maya import cmds # noqa import maya.mel as mel # noqa import pyblish.api from openpype.pipeline import publish from openpype.hosts.maya.api.lib import maintained_selection from openpype.hosts.maya.api import fbx class ExtractFBX(publish.Extractor): """Extract FBX from Maya. This extracts reproducible FBX exports ignoring any of the settings set on the local machine in the FBX export options window. """ order = pyblish.api.ExtractorOrder label = "Extract FBX" families = ["fbx"] def process(self, instance): fbx_exporter = fbx.FBXExtractor(log=self.log) # Define output path staging_dir = self.staging_dir(instance) filename = "{0}.fbx".format(instance.name) path = os.path.join(staging_dir, filename) # The export requires forward slashes because we need # to format it into a string in a mel expression path = path.replace('\\', '/') self.log.debug("Extracting FBX to: {0}".format(path)) members = instance.data["setMembers"] self.log.debug("Members: {0}".format(members)) self.log.debug("Instance: {0}".format(instance[:])) fbx_exporter.set_options_from_instance(instance) # Export with maintained_selection(): fbx_exporter.export(members, path) cmds.select(members, r=1, noExpand=True) mel.eval('FBXExport -f "{}" -s'.format(path)) if "representations" not in instance.data: instance.data["representations"] = [] representation = { 'name': 'fbx', 'ext': 'fbx', 'files': filename, "stagingDir": staging_dir, } instance.data["representations"].append(representation) self.log.debug("Extract FBX successful to: {0}".format(path)) ================================================ FILE: openpype/hosts/maya/plugins/publish/extract_fbx_animation.py ================================================ # -*- coding: utf-8 -*- import os from maya import cmds # noqa import pyblish.api from openpype.pipeline import publish from openpype.hosts.maya.api import fbx from openpype.hosts.maya.api.lib import ( namespaced, get_namespace, strip_namespace ) class ExtractFBXAnimation(publish.Extractor): """Extract Rig in FBX format from Maya. This extracts the rig in fbx with the constraints and referenced asset content included. This also optionally extract animated rig in fbx with geometries included. """ order = pyblish.api.ExtractorOrder label = "Extract Animation (FBX)" hosts = ["maya"] families = ["animation.fbx"] def process(self, instance): # Define output path staging_dir = self.staging_dir(instance) filename = "{0}.fbx".format(instance.name) path = os.path.join(staging_dir, filename) path = path.replace("\\", "/") fbx_exporter = fbx.FBXExtractor(log=self.log) out_members = instance.data.get("animated_skeleton", []) # Export instance.data["constraints"] = True instance.data["skeletonDefinitions"] = True instance.data["referencedAssetsContent"] = True fbx_exporter.set_options_from_instance(instance) # Export from the rig's namespace so that the exported # FBX does not include the namespace but preserves the node # names as existing in the rig workfile if not out_members: skeleton_set = [ i for i in instance if i.endswith("skeletonAnim_SET") ] self.log.debug( "Top group of animated skeleton not found in " "{}.\nSkipping fbx animation extraction.".format(skeleton_set)) return namespace = get_namespace(out_members[0]) relative_out_members = [ strip_namespace(node, namespace) for node in out_members ] with namespaced( ":" + namespace, new=False, relative_names=True ) as namespace: fbx_exporter.export(relative_out_members, path) representations = instance.data.setdefault("representations", []) representations.append({ 'name': 'fbx', 'ext': 'fbx', 'files': filename, "stagingDir": staging_dir }) self.log.debug( "Extracted FBX animation to: {0}".format(path)) ================================================ FILE: openpype/hosts/maya/plugins/publish/extract_gltf.py ================================================ import os from maya import cmds, mel import pyblish.api from openpype.pipeline import publish from openpype.hosts.maya.api import lib from openpype.hosts.maya.api.gltf import extract_gltf class ExtractGLB(publish.Extractor): order = pyblish.api.ExtractorOrder hosts = ["maya"] label = "Extract GLB" families = ["gltf"] def process(self, instance): staging_dir = self.staging_dir(instance) filename = "{0}.glb".format(instance.name) path = os.path.join(staging_dir, filename) cmds.loadPlugin("maya2glTF", quiet=True) nodes = instance[:] start_frame = instance.data('frameStart') or \ int(cmds.playbackOptions(query=True, animationStartTime=True))# noqa end_frame = instance.data('frameEnd') or \ int(cmds.playbackOptions(query=True, animationEndTime=True)) # noqa fps = mel.eval('currentTimeUnitToFPS()') options = { "sno": True, # selectedNodeOnly "nbu": True, # .bin instead of .bin0 "ast": start_frame, "aet": end_frame, "afr": fps, "dsa": 1, "acn": instance.name, "glb": True, "vno": True # visibleNodeOnly } self.log.debug("Extracting GLB to: {}".format(path)) with lib.maintained_selection(): cmds.select(nodes, hi=True, noExpand=True) extract_gltf(staging_dir, instance.name, **options) if "representations" not in instance.data: instance.data["representations"] = [] representation = { 'name': 'glb', 'ext': 'glb', 'files': filename, "stagingDir": staging_dir, } instance.data["representations"].append(representation) self.log.debug("Extract GLB successful to: {0}".format(path)) ================================================ FILE: openpype/hosts/maya/plugins/publish/extract_gpu_cache.py ================================================ import json from maya import cmds from openpype.pipeline import publish class ExtractGPUCache(publish.Extractor): """Extract the content of the instance to a GPU cache file.""" label = "GPU Cache" hosts = ["maya"] families = ["model", "animation", "pointcache"] step = 1.0 stepSave = 1 optimize = True optimizationThreshold = 40000 optimizeAnimationsForMotionBlur = True writeMaterials = True useBaseTessellation = True def process(self, instance): cmds.loadPlugin("gpuCache", quiet=True) staging_dir = self.staging_dir(instance) filename = "{}_gpu_cache".format(instance.name) # Write out GPU cache file. kwargs = { "directory": staging_dir, "fileName": filename, "saveMultipleFiles": False, "simulationRate": self.step, "sampleMultiplier": self.stepSave, "optimize": self.optimize, "optimizationThreshold": self.optimizationThreshold, "optimizeAnimationsForMotionBlur": ( self.optimizeAnimationsForMotionBlur ), "writeMaterials": self.writeMaterials, "useBaseTessellation": self.useBaseTessellation } self.log.debug( "Extract {} with:\n{}".format( instance[:], json.dumps(kwargs, indent=4, sort_keys=True) ) ) cmds.gpuCache(instance[:], **kwargs) if "representations" not in instance.data: instance.data["representations"] = [] representation = { "name": "gpu_cache", "ext": "abc", "files": filename + ".abc", "stagingDir": staging_dir, "outputName": "gpu_cache" } instance.data["representations"].append(representation) self.log.debug( "Extracted instance {} to: {}".format(instance.name, staging_dir) ) ================================================ FILE: openpype/hosts/maya/plugins/publish/extract_import_reference.py ================================================ import os import sys from maya import cmds import pyblish.api import tempfile from openpype.lib import run_subprocess from openpype.pipeline import publish from openpype.pipeline.publish import OptionalPyblishPluginMixin from openpype.hosts.maya.api import lib class ExtractImportReference(publish.Extractor, OptionalPyblishPluginMixin): """ Extract the scene with imported reference. The temp scene with imported reference is published for rendering if this extractor is activated """ label = "Extract Import Reference" order = pyblish.api.ExtractorOrder - 0.48 hosts = ["maya"] families = ["renderlayer", "workfile"] optional = True tmp_format = "_tmp" @classmethod def apply_settings(cls, project_settings): cls.active = project_settings["deadline"]["publish"]["MayaSubmitDeadline"]["import_reference"] # noqa def process(self, instance): if not self.is_active(instance.data): return ext_mapping = ( instance.context.data["project_settings"]["maya"]["ext_mapping"] ) if ext_mapping: self.log.debug("Looking in settings for scene type ...") # use extension mapping for first family found for family in self.families: try: self.scene_type = ext_mapping[family] self.log.debug( "Using {} as scene type".format(self.scene_type)) break except KeyError: # set scene type to ma self.scene_type = "ma" _scene_type = ("mayaAscii" if self.scene_type == "ma" else "mayaBinary") dir_path = self.staging_dir(instance) # named the file with imported reference if instance.name == "Main": return tmp_name = instance.name + self.tmp_format current_name = cmds.file(query=True, sceneName=True) ref_scene_name = "{0}.{1}".format(tmp_name, self.scene_type) reference_path = os.path.join(dir_path, ref_scene_name) tmp_path = os.path.dirname(current_name) + "/" + ref_scene_name self.log.debug("Performing extraction..") # This generates script for mayapy to take care of reference # importing outside current session. It is passing current scene # name and destination scene name. script = (""" # -*- coding: utf-8 -*- '''Script to import references to given scene.''' import maya.standalone maya.standalone.initialize() # scene names filled by caller current_name = "{current_name}" ref_scene_name = "{ref_scene_name}" print(">>> Opening {{}} ...".format(current_name)) cmds.file(current_name, open=True, force=True) print(">>> Processing references") all_reference = cmds.file(q=True, reference=True) or [] for ref in all_reference: if cmds.referenceQuery(ref, il=True): cmds.file(ref, importReference=True) nested_ref = cmds.file(q=True, reference=True) if nested_ref: for new_ref in nested_ref: if new_ref not in all_reference: all_reference.append(new_ref) print(">>> Finish importing references") print(">>> Saving scene as {{}}".format(ref_scene_name)) cmds.file(rename=ref_scene_name) cmds.file(save=True, force=True) print("*** Done") """).format(current_name=current_name, ref_scene_name=tmp_path) mayapy_exe = os.path.join(os.getenv("MAYA_LOCATION"), "bin", "mayapy") if sys.platform == "windows": mayapy_exe += ".exe" mayapy_exe = os.path.normpath(mayapy_exe) # can't use TemporaryNamedFile as that can't be opened in another # process until handles are closed by context manager. with tempfile.TemporaryDirectory() as tmp_dir_name: tmp_script_path = os.path.join(tmp_dir_name, "import_ref.py") self.log.debug("Using script file: {}".format(tmp_script_path)) with open(tmp_script_path, "wt") as tmp: tmp.write(script) try: run_subprocess([mayapy_exe, tmp_script_path]) except Exception: self.log.error("Import reference failed", exc_info=True) raise with lib.maintained_selection(): cmds.select(all=True, noExpand=True) cmds.file(reference_path, force=True, typ=_scene_type, exportSelected=True, channels=True, constraints=True, shader=True, expressions=True, constructionHistory=True) instance.context.data["currentFile"] = tmp_path if "files" not in instance.data: instance.data["files"] = [] instance.data["files"].append(ref_scene_name) if instance.data.get("representations") is None: instance.data["representations"] = [] ref_representation = { "name": self.scene_type, "ext": self.scene_type, "files": ref_scene_name, "stagingDir": os.path.dirname(current_name), "outputName": "imported" } self.log.debug(ref_representation) instance.data["representations"].append(ref_representation) self.log.debug("Extracted instance '%s' to : '%s'" % (ref_scene_name, reference_path)) ================================================ FILE: openpype/hosts/maya/plugins/publish/extract_layout.py ================================================ import math import os import json from maya import cmds from maya.api import OpenMaya as om from openpype.client import get_representation_by_id from openpype.pipeline import publish class ExtractLayout(publish.Extractor): """Extract a layout.""" label = "Extract Layout" hosts = ["maya"] families = ["layout"] project_container = "AVALON_CONTAINERS" optional = True def process(self, instance): # Define extract output file path stagingdir = self.staging_dir(instance) # Perform extraction self.log.debug("Performing extraction..") if "representations" not in instance.data: instance.data["representations"] = [] json_data = [] # TODO representation queries can be refactored to be faster project_name = instance.context.data["projectName"] for asset in cmds.sets(str(instance), query=True): # Find the container project_container = self.project_container container_list = cmds.ls(project_container) if len(container_list) == 0: self.log.warning("Project container is not found!") self.log.warning("The asset(s) may not be properly loaded after published") # noqa continue grp_loaded_ass = instance.data.get("groupLoadedAssets", False) if grp_loaded_ass: asset_list = cmds.listRelatives(asset, children=True) for asset in asset_list: grp_name = asset.split(':')[0] else: grp_name = asset.split(':')[0] containers = cmds.ls("{}*_CON".format(grp_name)) if len(containers) == 0: self.log.warning("{} isn't from the loader".format(asset)) self.log.warning("It may not be properly loaded after published") # noqa continue container = containers[0] representation_id = cmds.getAttr( "{}.representation".format(container)) representation = get_representation_by_id( project_name, representation_id, fields=["parent", "context.family"] ) self.log.debug(representation) version_id = representation.get("parent") family = representation.get("context").get("family") json_element = { "family": family, "instance_name": cmds.getAttr( "{}.namespace".format(container)), "representation": str(representation_id), "version": str(version_id) } loc = cmds.xform(asset, query=True, translation=True) rot = cmds.xform(asset, query=True, rotation=True, euler=True) scl = cmds.xform(asset, query=True, relative=True, scale=True) json_element["transform"] = { "translation": { "x": loc[0], "y": loc[1], "z": loc[2] }, "rotation": { "x": math.radians(rot[0]), "y": math.radians(rot[1]), "z": math.radians(rot[2]) }, "scale": { "x": scl[0], "y": scl[1], "z": scl[2] } } row_length = 4 t_matrix_list = cmds.xform(asset, query=True, matrix=True) transform_mm = om.MMatrix(t_matrix_list) transform = om.MTransformationMatrix(transform_mm) t = transform.translation(om.MSpace.kWorld) t = om.MVector(t.x, t.z, -t.y) transform.setTranslation(t, om.MSpace.kWorld) transform.rotateBy( om.MEulerRotation(math.radians(-90), 0, 0), om.MSpace.kWorld) transform.scaleBy([1.0, 1.0, -1.0], om.MSpace.kObject) t_matrix_list = list(transform.asMatrix()) t_matrix = [] for i in range(0, len(t_matrix_list), row_length): t_matrix.append(t_matrix_list[i:i + row_length]) json_element["transform_matrix"] = [ list(row) for row in t_matrix ] basis_list = [ 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, -1, 0, 0, 0, 0, 1 ] basis_mm = om.MMatrix(basis_list) basis = om.MTransformationMatrix(basis_mm) b_matrix_list = list(basis.asMatrix()) b_matrix = [] for i in range(0, len(b_matrix_list), row_length): b_matrix.append(b_matrix_list[i:i + row_length]) json_element["basis"] = [] for row in b_matrix: json_element["basis"].append(list(row)) json_data.append(json_element) json_filename = "{}.json".format(instance.name) json_path = os.path.join(stagingdir, json_filename) with open(json_path, "w+") as file: json.dump(json_data, fp=file, indent=2) json_representation = { 'name': 'json', 'ext': 'json', 'files': json_filename, "stagingDir": stagingdir, } instance.data["representations"].append(json_representation) self.log.debug("Extracted instance '%s' to: %s", instance.name, json_representation) ================================================ FILE: openpype/hosts/maya/plugins/publish/extract_look.py ================================================ # -*- coding: utf-8 -*- """Maya look extractor.""" import sys from abc import ABCMeta, abstractmethod from collections import OrderedDict import contextlib import json import logging import os import tempfile import six import attr import pyblish.api from maya import cmds # noqa from openpype.lib import ( find_executable, source_hash, run_subprocess, get_oiio_tool_args, ToolNotFoundError, ) from openpype.pipeline import legacy_io, publish, KnownPublishError from openpype.hosts.maya.api import lib from openpype import AYON_SERVER_ENABLED # Modes for transfer COPY = 1 HARDLINK = 2 @attr.s class TextureResult(object): """The resulting texture of a processed file for a resource""" # Path to the file path = attr.ib() # Colorspace of the resulting texture. This might not be the input # colorspace of the texture if a TextureProcessor has processed the file. colorspace = attr.ib() # Hash generated for the texture using openpype.lib.source_hash file_hash = attr.ib() # The transfer mode, e.g. COPY or HARDLINK transfer_mode = attr.ib() def find_paths_by_hash(texture_hash): """Find the texture hash key in the dictionary. All paths that originate from it. Args: texture_hash (str): Hash of the texture. Return: str: path to texture if found. """ if AYON_SERVER_ENABLED: raise KnownPublishError( "This is a bug. \"find_paths_by_hash\" is not compatible with " "AYON." ) key = "data.sourceHashes.{0}".format(texture_hash) return legacy_io.distinct(key, {"type": "version"}) @contextlib.contextmanager def no_workspace_dir(): """Force maya to a fake temporary workspace directory. Note: This is not maya.cmds.workspace 'rootDirectory' but the 'directory' This helps to avoid Maya automatically remapping image paths to files relative to the currently set directory. """ # Store current workspace original = cmds.workspace(query=True, directory=True) # Set a fake workspace fake_workspace_dir = tempfile.mkdtemp() cmds.workspace(directory=fake_workspace_dir) try: yield finally: try: cmds.workspace(directory=original) except RuntimeError: # If the original workspace directory didn't exist either # ignore the fact that it fails to reset it to the old path pass # Remove the temporary directory os.rmdir(fake_workspace_dir) @six.add_metaclass(ABCMeta) class TextureProcessor: extension = None def __init__(self, log=None): if log is None: log = logging.getLogger(self.__class__.__name__) self.log = log def apply_settings(self, system_settings, project_settings): """Apply OpenPype system/project settings to the TextureProcessor Args: system_settings (dict): OpenPype system settings project_settings (dict): OpenPype project settings Returns: None """ pass @abstractmethod def process(self, source, colorspace, color_management, staging_dir): """Process the `source` texture. Must be implemented on inherited class. This must always return a TextureResult even when it does not generate a texture. If it doesn't generate a texture then it should return a TextureResult using the input path and colorspace. Args: source (str): Path to source file. colorspace (str): Colorspace of the source file. color_management (dict): Maya Color management data from `lib.get_color_management_preferences` staging_dir (str): Output directory to write to. Returns: TextureResult: The resulting texture information. """ pass def __repr__(self): # Log instance as class name return self.__class__.__name__ class MakeRSTexBin(TextureProcessor): """Make `.rstexbin` using `redshiftTextureProcessor`""" extension = ".rstexbin" def process(self, source, colorspace, color_management, staging_dir): texture_processor_path = self.get_redshift_tool( "redshiftTextureProcessor" ) if not texture_processor_path: raise KnownPublishError("Must have Redshift available.") subprocess_args = [ texture_processor_path, source ] # if color management is enabled we pass color space information if color_management["enabled"]: config_path = color_management["config"] if not os.path.exists(config_path): raise RuntimeError("OCIO config not found at: " "{}".format(config_path)) if not os.getenv("OCIO"): self.log.debug( "OCIO environment variable not set." "Setting it with OCIO config from Maya." ) os.environ["OCIO"] = config_path self.log.debug("converting colorspace {0} to redshift render " "colorspace".format(colorspace)) subprocess_args.extend(["-cs", colorspace]) hash_args = ["rstex"] texture_hash = source_hash(source, *hash_args) # Redshift stores the output texture next to the input but with # the extension replaced to `.rstexbin` basename, ext = os.path.splitext(source) destination = "{}{}".format(basename, self.extension) self.log.debug(" ".join(subprocess_args)) try: run_subprocess(subprocess_args, logger=self.log) except Exception: self.log.error("Texture .rstexbin conversion failed", exc_info=True) six.reraise(*sys.exc_info()) return TextureResult( path=destination, file_hash=texture_hash, colorspace=colorspace, transfer_mode=COPY ) @staticmethod def get_redshift_tool(tool_name): """Path to redshift texture processor. On Windows it adds .exe extension if missing from tool argument. Args: tool_name (string): Tool name. Returns: str: Full path to redshift texture processor executable. """ if "REDSHIFT_COREDATAPATH" not in os.environ: raise RuntimeError("Must have Redshift available.") redshift_tool_path = os.path.join( os.environ["REDSHIFT_COREDATAPATH"], "bin", tool_name ) return find_executable(redshift_tool_path) class MakeTX(TextureProcessor): """Make `.tx` using `maketx` with some default settings. Some hardcoded arguments passed to `maketx` are based on the defaults used in Arnold's txManager tool. """ extension = ".tx" def __init__(self, log=None): super(MakeTX, self).__init__(log=log) self.extra_args = [] def apply_settings(self, system_settings, project_settings): # Allow extra maketx arguments from project settings args_settings = ( project_settings["maya"]["publish"] .get("ExtractLook", {}).get("maketx_arguments", []) ) extra_args = [] for arg_data in args_settings: argument = arg_data["argument"] parameters = arg_data["parameters"] if not argument: self.log.debug("Ignoring empty parameter from " "`maketx_arguments` setting..") continue extra_args.append(argument) extra_args.extend(parameters) self.extra_args = extra_args def process(self, source, colorspace, color_management, staging_dir): """Process the texture. This function requires the `maketx` executable to be available in an OpenImageIO toolset detectable by OpenPype. Args: source (str): Path to source file. colorspace (str): Colorspace of the source file. color_management (dict): Maya Color management data from `lib.get_color_management_preferences` staging_dir (str): Output directory to write to. Returns: TextureResult: The resulting texture information. """ try: maketx_args = get_oiio_tool_args("maketx") except ToolNotFoundError: raise KnownPublishError( "OpenImageIO is not available on the machine") # Define .tx filepath in staging if source file is not .tx fname, ext = os.path.splitext(os.path.basename(source)) if ext == ".tx": # Do nothing if the source file is already a .tx file. return TextureResult( path=source, file_hash=source_hash(source), colorspace=colorspace, transfer_mode=COPY ) # Hardcoded default arguments for maketx conversion based on Arnold's # txManager in Maya args = [ # unpremultiply before conversion (recommended when alpha present) "--unpremult", # use oiio-optimized settings for tile-size, planarconfig, metadata "--oiio", "--filter", "lanczos3", ] if color_management["enabled"]: config_path = color_management["config"] if not os.path.exists(config_path): raise RuntimeError("OCIO config not found at: " "{}".format(config_path)) render_colorspace = color_management["rendering_space"] self.log.debug("tx: converting colorspace {0} " "-> {1}".format(colorspace, render_colorspace)) args.extend(["--colorconvert", colorspace, render_colorspace]) args.extend(["--colorconfig", config_path]) else: # Maya Color management is disabled. We cannot rely on an OCIO self.log.debug("tx: Maya color management is disabled. No color " "conversion will be applied to .tx conversion for: " "{}".format(source)) # Assume linear render_colorspace = "linear" # Note: The texture hash is only reliable if we include any potential # conversion arguments provide to e.g. `maketx` hash_args = ["maketx"] + args + self.extra_args texture_hash = source_hash(source, *hash_args) # Ensure folder exists resources_dir = os.path.join(staging_dir, "resources") if not os.path.exists(resources_dir): os.makedirs(resources_dir) self.log.debug("Generating .tx file for %s .." % source) subprocess_args = maketx_args + [ "-v", # verbose "-u", # update mode # --checknan doesn't influence the output file but aborts the # conversion if it finds any. So we can avoid it for the file hash "--checknan", source ] subprocess_args.extend(args) if self.extra_args: subprocess_args.extend(self.extra_args) # Add source hash attribute after other arguments for log readability # Note: argument is excluded from the hash since it is the hash itself subprocess_args.extend([ "--sattrib", "sourceHash", texture_hash ]) destination = os.path.join(resources_dir, fname + ".tx") subprocess_args.extend(["-o", destination]) # We want to make sure we are explicit about what OCIO config gets # used. So when we supply no --colorconfig flag that no fallback to # an OCIO env var occurs. env = os.environ.copy() env.pop("OCIO", None) self.log.debug(" ".join(subprocess_args)) try: run_subprocess(subprocess_args, env=env) except Exception: self.log.error("Texture maketx conversion failed", exc_info=True) raise return TextureResult( path=destination, file_hash=texture_hash, colorspace=render_colorspace, transfer_mode=COPY ) @staticmethod def _has_arnold(): """Return whether the arnold package is available and importable.""" try: import arnold # noqa: F401 return True except (ImportError, ModuleNotFoundError): return False class ExtractLook(publish.Extractor): """Extract Look (Maya Scene + JSON) Only extracts the sets (shadingEngines and alike) alongside a .json file that stores it relationships for the sets and "attribute" data for the instance members. """ label = "Extract Look (Maya Scene + JSON)" hosts = ["maya"] families = ["look", "mvLook"] order = pyblish.api.ExtractorOrder + 0.2 scene_type = "ma" look_data_type = "json" def get_maya_scene_type(self, instance): """Get Maya scene type from settings. Args: instance (pyblish.api.Instance): Instance with collected project settings. """ ext_mapping = ( instance.context.data["project_settings"]["maya"]["ext_mapping"] ) if ext_mapping: self.log.debug("Looking in settings for scene type ...") # use extension mapping for first family found for family in self.families: try: self.scene_type = ext_mapping[family] self.log.debug( "Using {} as scene type".format(self.scene_type)) break except KeyError: # no preset found pass return "mayaAscii" if self.scene_type == "ma" else "mayaBinary" def process(self, instance): """Plugin entry point. Args: instance: Instance to process. """ _scene_type = self.get_maya_scene_type(instance) # Define extract output file path dir_path = self.staging_dir(instance) maya_fname = "{0}.{1}".format(instance.name, self.scene_type) json_fname = "{0}.{1}".format(instance.name, self.look_data_type) maya_path = os.path.join(dir_path, maya_fname) json_path = os.path.join(dir_path, json_fname) # Remove all members of the sets so they are not included in the # exported file by accident self.log.debug("Processing sets..") lookdata = instance.data["lookData"] relationships = lookdata["relationships"] sets = list(relationships.keys()) if not sets: self.log.debug("No sets found for the look") return # Specify texture processing executables to activate # TODO: Load these more dynamically once we support more processors processors = [] context = instance.context for key, Processor in { # Instance data key to texture processor mapping "maketx": MakeTX, "rstex": MakeRSTexBin }.items(): if instance.data.get(key, False): processor = Processor(log=self.log) processor.apply_settings(context.data["system_settings"], context.data["project_settings"]) processors.append(processor) if processors: self.log.debug("Collected texture processors: " "{}".format(processors)) self.log.debug("Processing resources..") results = self.process_resources(instance, staging_dir=dir_path, processors=processors) transfers = results["fileTransfers"] hardlinks = results["fileHardlinks"] hashes = results["fileHashes"] remap = results["attrRemap"] # Extract in correct render layer self.log.debug("Extracting look maya scene file: {}".format(maya_path)) layer = instance.data.get("renderlayer", "defaultRenderLayer") with lib.renderlayer(layer): # TODO: Ensure membership edits don't become renderlayer overrides with lib.empty_sets(sets, force=True): # To avoid Maya trying to automatically remap the file # textures relative to the `workspace -directory` we force # it to a fake temporary workspace. This fixes textures # getting incorrectly remapped. with no_workspace_dir(): with lib.attribute_values(remap): with lib.maintained_selection(): cmds.select(sets, noExpand=True) cmds.file( maya_path, force=True, typ=_scene_type, exportSelected=True, preserveReferences=False, channels=True, constraints=True, expressions=True, constructionHistory=True, ) # Write the JSON data data = { "attributes": lookdata["attributes"], "relationships": relationships } self.log.debug("Extracting json file: {}".format(json_path)) with open(json_path, "w") as f: json.dump(data, f) if "files" not in instance.data: instance.data["files"] = [] if "hardlinks" not in instance.data: instance.data["hardlinks"] = [] if "transfers" not in instance.data: instance.data["transfers"] = [] instance.data["files"].append(maya_fname) instance.data["files"].append(json_fname) if instance.data.get("representations") is None: instance.data["representations"] = [] instance.data["representations"].append( { "name": self.scene_type, "ext": self.scene_type, "files": os.path.basename(maya_fname), "stagingDir": os.path.dirname(maya_fname), } ) instance.data["representations"].append( { "name": self.look_data_type, "ext": self.look_data_type, "files": os.path.basename(json_fname), "stagingDir": os.path.dirname(json_fname), } ) # Set up the resources transfers/links for the integrator instance.data["transfers"].extend(transfers) instance.data["hardlinks"].extend(hardlinks) # Source hash for the textures instance.data["sourceHashes"] = hashes self.log.debug("Extracted instance '%s' to: %s" % (instance.name, maya_path)) def _set_resource_result_colorspace(self, resource, colorspace): """Update resource resulting colorspace after texture processing""" if "result_color_space" in resource: if resource["result_color_space"] == colorspace: return self.log.warning( "Resource already has a resulting colorspace but is now " "being overridden to a new one: {} -> {}".format( resource["result_color_space"], colorspace ) ) resource["result_color_space"] = colorspace def process_resources(self, instance, staging_dir, processors): """Process all resources in the instance. It is assumed that all resources are nodes using file textures. Extract the textures to transfer, possibly convert with maketx and remap the node paths to the destination path. Note that a source might be included more than once amongst the resources as they could be the input file to multiple nodes. """ resources = instance.data["resources"] color_management = lib.get_color_management_preferences() # TODO: Temporary disable all hardlinking, due to the feature not being # used or properly working. self.log.info( "Forcing copy instead of hardlink." ) force_copy = True if not force_copy and platform.system().lower() == "windows": # Temporary fix to NOT create hardlinks on windows machines self.log.warning( "Forcing copy instead of hardlink due to issues on Windows..." ) force_copy = True destinations_cache = {} def get_resource_destination_cached(path): """Get resource destination with cached result per filepath""" if path not in destinations_cache: destination = self.get_resource_destination( path, instance.data["resourcesDir"], processors) destinations_cache[path] = destination return destinations_cache[path] # Process all resource's individual files processed_files = {} transfers = [] hardlinks = [] hashes = {} remap = OrderedDict() for resource in resources: colorspace = resource["color_space"] for filepath in resource["files"]: filepath = os.path.normpath(filepath) if filepath in processed_files: # The file was already processed, likely due to usage by # another resource in the scene. We confirm here it # didn't do color spaces different than the current # resource. processed_file = processed_files[filepath] self.log.debug( "File was already processed. Likely used by another " "resource too: {}".format(filepath) ) if colorspace != processed_file["color_space"]: self.log.warning( "File '{}' was already processed using colorspace " "'{}' instead of the current resource's " "colorspace '{}'. The already processed texture " "result's colorspace '{}' will be used." "".format(filepath, colorspace, processed_file["color_space"], processed_file["result_color_space"])) self._set_resource_result_colorspace( resource, colorspace=processed_file["result_color_space"] ) continue texture_result = self._process_texture( filepath, processors=processors, staging_dir=staging_dir, force_copy=force_copy, color_management=color_management, colorspace=colorspace ) # Set the resulting color space on the resource self._set_resource_result_colorspace( resource, colorspace=texture_result.colorspace ) processed_files[filepath] = { "color_space": colorspace, "result_color_space": texture_result.colorspace, } source = texture_result.path destination = get_resource_destination_cached(source) if force_copy or texture_result.transfer_mode == COPY: transfers.append((source, destination)) self.log.debug('file will be copied {} -> {}'.format( source, destination)) elif texture_result.transfer_mode == HARDLINK: hardlinks.append((source, destination)) self.log.debug('file will be hardlinked {} -> {}'.format( source, destination)) # Store the hashes from hash to destination to include in the # database hashes[texture_result.file_hash] = destination # Set up remapping attributes for the node during the publish # The order of these can be important if one attribute directly # affects another, e.g. we set colorspace after filepath because # maya sometimes tries to guess the colorspace when changing # filepaths (which is avoidable, but we don't want to have those # attributes changed in the resulting publish) # Remap filepath to publish destination # TODO It would be much better if we could use the destination path # from the actual processed texture results, but since the # attribute will need to preserve tokens like , etc for # now we will define the output path from the attribute value # including the tokens to persist them. filepath_attr = resource["attribute"] remap[filepath_attr] = get_resource_destination_cached( resource["source"] ) # Preserve color space values (force value after filepath change) # This will also trigger in the same order at end of context to # ensure after context it's still the original value. node = resource["node"] if cmds.attributeQuery("colorSpace", node=node, exists=True): color_space_attr = "{}.colorSpace".format(node) remap[color_space_attr] = resource["result_color_space"] self.log.debug("Finished remapping destinations ...") return { "fileTransfers": transfers, "fileHardlinks": hardlinks, "fileHashes": hashes, "attrRemap": remap, } def get_resource_destination(self, filepath, resources_dir, processors): """Get resource destination path. This is utility function to change path if resource file name is changed by some external tool like `maketx`. Args: filepath (str): Resource source path resources_dir (str): Destination dir for resources in publish. processors (list): Texture processors converting resource. Returns: str: Path to resource file """ # Compute destination location basename, ext = os.path.splitext(os.path.basename(filepath)) # Get extension from the last processor for processor in reversed(processors): processor_ext = processor.extension if processor_ext and ext != processor_ext: self.log.debug("Processor {} overrides extension to '{}' " "for path: {}".format(processor, processor_ext, filepath)) ext = processor_ext break return os.path.join( resources_dir, basename + ext ) def _get_existing_hashed_texture(self, texture_hash): """Return the first found filepath from a texture hash""" # If source has been published before with the same settings, # then don't reprocess but hardlink from the original existing = find_paths_by_hash(texture_hash) if existing: source = next((p for p in existing if os.path.exists(p)), None) if source: return source else: self.log.warning( "Paths not found on disk, " "skipping hardlink: {}".format(existing) ) def _process_texture(self, filepath, processors, staging_dir, force_copy, color_management, colorspace): """Process a single texture file on disk for publishing. This will: 1. Check whether it's already published, if so it will do hardlink (if the texture hash is found and force copy is not enabled) 2. It will process the texture using the supplied texture processors like MakeTX and MakeRSTexBin if enabled. 3. Compute the destination path for the source file. Args: filepath (str): The source file path to process. processors (list): List of TextureProcessor processing the texture staging_dir (str): The staging directory to write to. force_copy (bool): Whether to force a copy even if a file hash might have existed already in the project, otherwise hardlinking the existing file is allowed. color_management (dict): Maya's Color Management settings from `lib.get_color_management_preferences` colorspace (str): The source colorspace of the resources this texture belongs to. Returns: TextureResult: The texture result information. """ if len(processors) > 1: raise KnownPublishError( "More than one texture processor not supported. " "Current processors enabled: {}".format(processors) ) for processor in processors: self.log.debug("Processing texture {} with processor {}".format( filepath, processor )) processed_result = processor.process(filepath, colorspace, color_management, staging_dir) if not processed_result: raise RuntimeError("Texture Processor {} returned " "no result.".format(processor)) self.log.debug("Generated processed " "texture: {}".format(processed_result.path)) # TODO: Currently all processors force copy instead of allowing # hardlinks using source hashes. This should be refactored return processed_result # No texture processing for this file texture_hash = source_hash(filepath) if not force_copy: existing = self._get_existing_hashed_texture(filepath) if existing: self.log.debug("Found hash in database, preparing hardlink..") return TextureResult( path=filepath, file_hash=texture_hash, colorspace=colorspace, transfer_mode=HARDLINK ) return TextureResult( path=filepath, file_hash=texture_hash, colorspace=colorspace, transfer_mode=COPY ) class ExtractModelRenderSets(ExtractLook): """Extract model render attribute sets as model metadata Only extracts the render attrib sets (NO shadingEngines) alongside a .json file that stores it relationships for the sets and "attribute" data for the instance members. """ label = "Model Render Sets" hosts = ["maya"] families = ["model"] scene_type_prefix = "meta.render." look_data_type = "meta.render.json" def get_maya_scene_type(self, instance): typ = super(ExtractModelRenderSets, self).get_maya_scene_type(instance) # add prefix self.scene_type = self.scene_type_prefix + self.scene_type return typ ================================================ FILE: openpype/hosts/maya/plugins/publish/extract_maya_scene_raw.py ================================================ # -*- coding: utf-8 -*- """Extract data as Maya scene (raw).""" import os from maya import cmds from openpype.hosts.maya.api.lib import maintained_selection from openpype.pipeline import AVALON_CONTAINER_ID, publish from openpype.pipeline.publish import OpenPypePyblishPluginMixin from openpype.lib import BoolDef class ExtractMayaSceneRaw(publish.Extractor, OpenPypePyblishPluginMixin): """Extract as Maya Scene (raw). This will preserve all references, construction history, etc. """ label = "Maya Scene (Raw)" hosts = ["maya"] families = ["mayaAscii", "mayaScene", "setdress", "layout", "camerarig"] scene_type = "ma" @classmethod def get_attribute_defs(cls): return [ BoolDef( "preserve_references", label="Preserve References", tooltip=( "When enabled references will still be references " "in the published file.\nWhen disabled the references " "are imported into the published file generating a " "file without references." ), default=True ) ] def process(self, instance): """Plugin entry point.""" ext_mapping = ( instance.context.data["project_settings"]["maya"]["ext_mapping"] ) if ext_mapping: self.log.debug("Looking in settings for scene type ...") # use extension mapping for first family found for family in self.families: try: self.scene_type = ext_mapping[family] self.log.debug( "Using {} as scene type".format(self.scene_type)) break except KeyError: # no preset found pass # Define extract output file path dir_path = self.staging_dir(instance) filename = "{0}.{1}".format(instance.name, self.scene_type) path = os.path.join(dir_path, filename) # Whether to include all nodes in the instance (including those from # history) or only use the exact set members members_only = instance.data.get("exactSetMembersOnly", False) if members_only: members = instance.data.get("setMembers", list()) if not members: raise RuntimeError("Can't export 'exact set members only' " "when set is empty.") else: members = instance[:] selection = members if set(self.add_for_families).intersection( set(instance.data.get("families", []))) or \ instance.data.get("family") in self.add_for_families: selection += self._get_loaded_containers(members) # Perform extraction self.log.debug("Performing extraction ...") attribute_values = self.get_attr_values_from_data( instance.data ) with maintained_selection(): cmds.select(selection, noExpand=True) cmds.file(path, force=True, typ="mayaAscii" if self.scene_type == "ma" else "mayaBinary", # noqa: E501 exportSelected=True, preserveReferences=attribute_values[ "preserve_references" ], constructionHistory=True, shader=True, constraints=True, expressions=True) if "representations" not in instance.data: instance.data["representations"] = [] representation = { 'name': self.scene_type, 'ext': self.scene_type, 'files': filename, "stagingDir": dir_path } instance.data["representations"].append(representation) self.log.debug("Extracted instance '%s' to: %s" % (instance.name, path)) @staticmethod def _get_loaded_containers(members): # type: (list) -> list refs_to_include = { cmds.referenceQuery(node, referenceNode=True) for node in members if cmds.referenceQuery(node, isNodeReferenced=True) } members_with_refs = refs_to_include.union(members) obj_sets = cmds.ls("*.id", long=True, type="objectSet", recursive=True, objectsOnly=True) loaded_containers = [] for obj_set in obj_sets: if not cmds.attributeQuery("id", node=obj_set, exists=True): continue id_attr = "{}.id".format(obj_set) if cmds.getAttr(id_attr) != AVALON_CONTAINER_ID: continue set_content = set(cmds.sets(obj_set, query=True)) if set_content.intersection(members_with_refs): loaded_containers.append(obj_set) return loaded_containers ================================================ FILE: openpype/hosts/maya/plugins/publish/extract_maya_usd.py ================================================ import os import six import json import contextlib from maya import cmds import pyblish.api from openpype.pipeline import publish from openpype.hosts.maya.api.lib import maintained_selection @contextlib.contextmanager def usd_export_attributes(nodes, attrs=None, attr_prefixes=None, mapping=None): """Define attributes for the given nodes that should be exported. MayaUSDExport will export custom attributes if the Maya node has a string attribute `USD_UserExportedAttributesJson` that provides an export mapping for the maya attributes. This context manager will try to autogenerate such an attribute during the export to include attributes for the export. Arguments: nodes (List[str]): Nodes to process. attrs (Optional[List[str]]): Full name of attributes to include. attr_prefixes (Optional[List[str]]): Prefixes of attributes to include. mapping (Optional[Dict[Dict]]): A mapping per attribute name for the conversion to a USD attribute, including renaming, defining type, converting attribute precision, etc. This match the usual `USD_UserExportedAttributesJson` json mapping of `mayaUSDExport`. When no mapping provided for an attribute it will use `{}` as value. Examples: >>> with usd_export_attributes( >>> ["pCube1"], attrs="myDoubleAttributeAsFloat", mapping={ >>> "myDoubleAttributeAsFloat": { >>> "usdAttrName": "my:namespace:attrib", >>> "translateMayaDoubleToUsdSinglePrecision": True, >>> } >>> }) """ # todo: this might be better done with a custom export chaser # see `chaser` argument for `mayaUSDExport` import maya.api.OpenMaya as om if not attrs and not attr_prefixes: # context manager does nothing yield return if attrs is None: attrs = [] if attr_prefixes is None: attr_prefixes = [] if mapping is None: mapping = {} usd_json_attr = "USD_UserExportedAttributesJson" strings = attrs + ["{}*".format(prefix) for prefix in attr_prefixes] context_state = {} for node in set(nodes): node_attrs = cmds.listAttr(node, st=strings) if not node_attrs: # Nothing to do for this node continue node_attr_data = {} for node_attr in set(node_attrs): node_attr_data[node_attr] = mapping.get(node_attr, {}) if cmds.attributeQuery(usd_json_attr, node=node, exists=True): existing_node_attr_value = cmds.getAttr( "{}.{}".format(node, usd_json_attr) ) if existing_node_attr_value and existing_node_attr_value != "{}": # Any existing attribute mappings in an existing # `USD_UserExportedAttributesJson` attribute always take # precedence over what this function tries to imprint existing_node_attr_data = json.loads(existing_node_attr_value) node_attr_data.update(existing_node_attr_data) context_state[node] = json.dumps(node_attr_data) sel = om.MSelectionList() dg_mod = om.MDGModifier() fn_string = om.MFnStringData() fn_typed = om.MFnTypedAttribute() try: for node, value in context_state.items(): data = fn_string.create(value) sel.clear() if cmds.attributeQuery(usd_json_attr, node=node, exists=True): # Set the attribute value sel.add("{}.{}".format(node, usd_json_attr)) plug = sel.getPlug(0) dg_mod.newPlugValue(plug, data) else: # Create attribute with the value as default value sel.add(node) node_obj = sel.getDependNode(0) attr_obj = fn_typed.create(usd_json_attr, usd_json_attr, om.MFnData.kString, data) dg_mod.addAttribute(node_obj, attr_obj) dg_mod.doIt() yield finally: dg_mod.undoIt() class ExtractMayaUsd(publish.Extractor): """Extractor for Maya USD Asset data. Upon publish a .usd (or .usdz) asset file will typically be written. """ label = "Extract Maya USD Asset" hosts = ["maya"] families = ["mayaUsd"] @property def options(self): """Overridable options for Maya USD Export Given in the following format - {NAME: EXPECTED TYPE} If the overridden option's type does not match, the option is not included and a warning is logged. """ # TODO: Support more `mayaUSDExport` parameters return { "defaultUSDFormat": str, "stripNamespaces": bool, "mergeTransformAndShape": bool, "exportDisplayColor": bool, "exportColorSets": bool, "exportInstances": bool, "exportUVs": bool, "exportVisibility": bool, "exportComponentTags": bool, "exportRefsAsInstanceable": bool, "eulerFilter": bool, "renderableOnly": bool, "jobContext": (list, None) # optional list # "worldspace": bool, } @property def default_options(self): """The default options for Maya USD Export.""" # TODO: Support more `mayaUSDExport` parameters return { "defaultUSDFormat": "usdc", "stripNamespaces": False, "mergeTransformAndShape": False, "exportDisplayColor": False, "exportColorSets": True, "exportInstances": True, "exportUVs": True, "exportVisibility": True, "exportComponentTags": True, "exportRefsAsInstanceable": False, "eulerFilter": True, "renderableOnly": False, "jobContext": None # "worldspace": False } def parse_overrides(self, instance, options): """Inspect data of instance to determine overridden options""" for key in instance.data: if key not in self.options: continue # Ensure the data is of correct type value = instance.data[key] if isinstance(value, six.text_type): value = str(value) if not isinstance(value, self.options[key]): self.log.warning( "Overridden attribute {key} was of " "the wrong type: {invalid_type} " "- should have been {valid_type}".format( key=key, invalid_type=type(value).__name__, valid_type=self.options[key].__name__)) continue options[key] = value return options def filter_members(self, members): # Can be overridden by inherited classes return members def process(self, instance): # Load plugin first cmds.loadPlugin("mayaUsdPlugin", quiet=True) # Define output file path staging_dir = self.staging_dir(instance) file_name = "{0}.usd".format(instance.name) file_path = os.path.join(staging_dir, file_name) file_path = file_path.replace('\\', '/') # Parse export options options = self.default_options options = self.parse_overrides(instance, options) self.log.debug("Export options: {0}".format(options)) # Perform extraction self.log.debug("Performing extraction ...") members = instance.data("setMembers") self.log.debug('Collected objects: {}'.format(members)) members = self.filter_members(members) if not members: self.log.error('No members!') return start = instance.data["frameStartHandle"] end = instance.data["frameEndHandle"] def parse_attr_str(attr_str): result = list() for attr in attr_str.split(","): attr = attr.strip() if not attr: continue result.append(attr) return result attrs = parse_attr_str(instance.data.get("attr", "")) attrs += instance.data.get("userDefinedAttributes", []) attrs += ["cbId"] attr_prefixes = parse_attr_str(instance.data.get("attrPrefix", "")) self.log.debug('Exporting USD: {} / {}'.format(file_path, members)) with maintained_selection(): with usd_export_attributes(instance[:], attrs=attrs, attr_prefixes=attr_prefixes): cmds.mayaUSDExport(file=file_path, frameRange=(start, end), frameStride=instance.data.get("step", 1.0), exportRoots=members, **options) representation = { 'name': "usd", 'ext': "usd", 'files': file_name, 'stagingDir': staging_dir } instance.data.setdefault("representations", []).append(representation) self.log.debug( "Extracted instance {} to {}".format(instance.name, file_path) ) class ExtractMayaUsdAnim(ExtractMayaUsd): """Extractor for Maya USD Animation Sparse Cache data. This will extract the sparse cache data from the scene and generate a USD file with all the animation data. Upon publish a .usd sparse cache will be written. """ label = "Extract Maya USD Animation Sparse Cache" families = ["animation", "mayaUsd"] match = pyblish.api.Subset def filter_members(self, members): out_set = next((i for i in members if i.endswith("out_SET")), None) if out_set is None: self.log.warning("Expecting out_SET") return None members = cmds.ls(cmds.sets(out_set, query=True), long=True) return members ================================================ FILE: openpype/hosts/maya/plugins/publish/extract_model.py ================================================ # -*- coding: utf-8 -*- """Extract model as Maya Scene.""" import os from maya import cmds from openpype.pipeline import publish from openpype.hosts.maya.api import lib class ExtractModel(publish.Extractor, publish.OptionalPyblishPluginMixin): """Extract as Model (Maya Scene). Only extracts contents based on the original "setMembers" data to ensure publishing the least amount of required shapes. From that it only takes the shapes that are not intermediateObjects During export it sets a temporary context to perform a clean extraction. The context ensures: - Smooth preview is turned off for the geometry - Default shader is assigned (no materials are exported) - Remove display layers """ label = "Model (Maya Scene)" hosts = ["maya"] families = ["model"] scene_type = "ma" optional = True def process(self, instance): """Plugin entry point.""" if not self.is_active(instance.data): return ext_mapping = ( instance.context.data["project_settings"]["maya"]["ext_mapping"] ) if ext_mapping: self.log.debug("Looking in settings for scene type ...") # use extension mapping for first family found for family in self.families: try: self.scene_type = ext_mapping[family] self.log.debug( "Using {} as scene type".format(self.scene_type)) break except KeyError: # no preset found pass # Define extract output file path stagingdir = self.staging_dir(instance) filename = "{0}.{1}".format(instance.name, self.scene_type) path = os.path.join(stagingdir, filename) # Perform extraction self.log.debug("Performing extraction ...") # Get only the shape contents we need in such a way that we avoid # taking along intermediateObjects members = instance.data("setMembers") members = cmds.ls(members, dag=True, shapes=True, type=("mesh", "nurbsCurve"), noIntermediate=True, long=True) with lib.no_display_layers(instance): with lib.displaySmoothness(members, divisionsU=0, divisionsV=0, pointsWire=4, pointsShaded=1, polygonObject=1): with lib.shader(members, shadingEngine="initialShadingGroup"): with lib.maintained_selection(): cmds.select(members, noExpand=True) cmds.file(path, force=True, typ="mayaAscii" if self.scene_type == "ma" else "mayaBinary", # noqa: E501 exportSelected=True, preserveReferences=False, channels=False, constraints=False, expressions=False, constructionHistory=False) # Store reference for integration if "representations" not in instance.data: instance.data["representations"] = [] representation = { 'name': self.scene_type, 'ext': self.scene_type, 'files': filename, "stagingDir": stagingdir, } instance.data["representations"].append(representation) self.log.debug("Extracted instance '%s' to: %s" % (instance.name, path)) ================================================ FILE: openpype/hosts/maya/plugins/publish/extract_multiverse_look.py ================================================ import os from maya import cmds from openpype.pipeline import publish from openpype.hosts.maya.api.lib import maintained_selection class ExtractMultiverseLook(publish.Extractor): """Extractor for Multiverse USD look data. This will extract: - the shading networks that are assigned in MEOW as Maya material overrides to a Multiverse Compound - settings for a Multiverse Write Override operation. Relevant settings are visible in the Maya set node created by a Multiverse USD Look instance creator. The input data contained in the set is: - a single Multiverse Compound node with any number of Maya material overrides (typically set in MEOW) Upon publish two files will be written: - a .usda override file containing material assignment information - a .ma file containing shading networks Note: when layering the material assignment override on a loaded Compound, remember to set a matching attribute override with the namespace of the loaded compound in order for the material assignment to resolve. """ label = "Extract Multiverse USD Look" hosts = ["maya"] families = ["mvLook"] scene_type = "usda" file_formats = ["usda", "usd"] @property def options(self): """Overridable options for Multiverse USD Export Given in the following format - {NAME: EXPECTED TYPE} If the overridden option's type does not match, the option is not included and a warning is logged. """ return { "writeAll": bool, "writeTransforms": bool, "writeVisibility": bool, "writeAttributes": bool, "writeMaterials": bool, "writeVariants": bool, "writeVariantsDefinition": bool, "writeActiveState": bool, "writeNamespaces": bool, "numTimeSamples": int, "timeSamplesSpan": float } @property def default_options(self): """The default options for Multiverse USD extraction.""" return { "writeAll": False, "writeTransforms": False, "writeVisibility": False, "writeAttributes": True, "writeMaterials": True, "writeVariants": False, "writeVariantsDefinition": False, "writeActiveState": False, "writeNamespaces": True, "numTimeSamples": 1, "timeSamplesSpan": 0.0 } def get_file_format(self, instance): fileFormat = instance.data["fileFormat"] if fileFormat in range(len(self.file_formats)): self.scene_type = self.file_formats[fileFormat] def process(self, instance): # Load plugin first cmds.loadPlugin("MultiverseForMaya", quiet=True) # Define output file path staging_dir = self.staging_dir(instance) self.get_file_format(instance) file_name = "{0}.{1}".format(instance.name, self.scene_type) file_path = os.path.join(staging_dir, file_name) file_path = file_path.replace('\\', '/') # Parse export options options = self.default_options self.log.debug("Export options: {0}".format(options)) # Perform extraction self.log.debug("Performing extraction ...") with maintained_selection(): members = instance.data("setMembers") members = cmds.ls(members, dag=True, shapes=False, type="mvUsdCompoundShape", noIntermediate=True, long=True) self.log.debug('Collected object {}'.format(members)) if len(members) > 1: self.log.error('More than one member: {}'.format(members)) import multiverse over_write_opts = multiverse.OverridesWriteOptions() options_discard_keys = { "numTimeSamples", "timeSamplesSpan", "frameStart", "frameEnd", "handleStart", "handleEnd", "step", "fps" } for key, value in options.items(): if key in options_discard_keys: continue setattr(over_write_opts, key, value) for member in members: # @TODO: Make sure there is only one here. self.log.debug("Writing Override for '{}'".format(member)) multiverse.WriteOverrides(file_path, member, over_write_opts) if "representations" not in instance.data: instance.data["representations"] = [] representation = { 'name': self.scene_type, 'ext': self.scene_type, 'files': file_name, 'stagingDir': staging_dir } instance.data["representations"].append(representation) self.log.debug("Extracted instance {} to {}".format( instance.name, file_path)) ================================================ FILE: openpype/hosts/maya/plugins/publish/extract_multiverse_usd.py ================================================ import os import six from maya import cmds from maya import mel import pyblish.api from openpype.pipeline import publish from openpype.hosts.maya.api.lib import maintained_selection class ExtractMultiverseUsd(publish.Extractor): """Extractor for Multiverse USD Asset data. This will extract settings for a Multiverse Write Asset operation: they are visible in the Maya set node created by a Multiverse USD Asset instance creator. The input data contained in the set is: - a single hierarchy of Maya nodes. Multiverse supports a variety of Maya nodes such as transforms, mesh, curves, particles, instances, particle instancers, pfx, MASH, lights, cameras, joints, connected materials, shading networks etc. including many of their attributes. Upon publish a .usd (or .usdz) asset file will be typically written. """ label = "Extract Multiverse USD Asset" hosts = ["maya"] families = ["mvUsd"] scene_type = "usd" file_formats = ["usd", "usda", "usdz"] @property def options(self): """Overridable options for Multiverse USD Export Given in the following format - {NAME: EXPECTED TYPE} If the overridden option's type does not match, the option is not included and a warning is logged. """ return { "stripNamespaces": bool, "mergeTransformAndShape": bool, "writeAncestors": bool, "flattenParentXforms": bool, "writeSparseOverrides": bool, "useMetaPrimPath": bool, "customRootPath": str, "customAttributes": str, "nodeTypesToIgnore": str, "writeMeshes": bool, "writeCurves": bool, "writeParticles": bool, "writeCameras": bool, "writeLights": bool, "writeJoints": bool, "writeCollections": bool, "writePositions": bool, "writeNormals": bool, "writeUVs": bool, "writeColorSets": bool, "writeTangents": bool, "writeRefPositions": bool, "writeBlendShapes": bool, "writeDisplayColor": bool, "writeSkinWeights": bool, "writeMaterialAssignment": bool, "writeHardwareShader": bool, "writeShadingNetworks": bool, "writeTransformMatrix": bool, "writeUsdAttributes": bool, "writeInstancesAsReferences": bool, "timeVaryingTopology": bool, "customMaterialNamespace": str, "numTimeSamples": int, "timeSamplesSpan": float } @property def default_options(self): """The default options for Multiverse USD extraction.""" return { "stripNamespaces": False, "mergeTransformAndShape": False, "writeAncestors": False, "flattenParentXforms": False, "writeSparseOverrides": False, "useMetaPrimPath": False, "customRootPath": str(), "customAttributes": str(), "nodeTypesToIgnore": str(), "writeMeshes": True, "writeCurves": True, "writeParticles": True, "writeCameras": False, "writeLights": False, "writeJoints": False, "writeCollections": False, "writePositions": True, "writeNormals": True, "writeUVs": True, "writeColorSets": False, "writeTangents": False, "writeRefPositions": False, "writeBlendShapes": False, "writeDisplayColor": False, "writeSkinWeights": False, "writeMaterialAssignment": False, "writeHardwareShader": False, "writeShadingNetworks": False, "writeTransformMatrix": True, "writeUsdAttributes": False, "writeInstancesAsReferences": False, "timeVaryingTopology": False, "customMaterialNamespace": str(), "numTimeSamples": 1, "timeSamplesSpan": 0.0 } def parse_overrides(self, instance, options): """Inspect data of instance to determine overridden options""" for key in instance.data: if key not in self.options: continue # Ensure the data is of correct type value = instance.data[key] if isinstance(value, six.text_type): value = str(value) if not isinstance(value, self.options[key]): self.log.warning( "Overridden attribute {key} was of " "the wrong type: {invalid_type} " "- should have been {valid_type}".format( key=key, invalid_type=type(value).__name__, valid_type=self.options[key].__name__)) continue options[key] = value return options def get_default_options(self): return self.default_options def filter_members(self, members): return members def process(self, instance): # Load plugin first cmds.loadPlugin("MultiverseForMaya", quiet=True) # Define output file path staging_dir = self.staging_dir(instance) file_format = instance.data.get("fileFormat", 0) if file_format in range(len(self.file_formats)): self.scene_type = self.file_formats[file_format] file_name = "{0}.{1}".format(instance.name, self.scene_type) file_path = os.path.join(staging_dir, file_name) file_path = file_path.replace('\\', '/') # Parse export options options = self.get_default_options() options = self.parse_overrides(instance, options) self.log.debug("Export options: {0}".format(options)) # Perform extraction self.log.debug("Performing extraction ...") with maintained_selection(): members = instance.data("setMembers") self.log.debug('Collected objects: {}'.format(members)) members = self.filter_members(members) if not members: self.log.error('No members!') return self.log.debug(' - filtered: {}'.format(members)) import multiverse time_opts = None frame_start = instance.data['frameStart'] frame_end = instance.data['frameEnd'] if frame_end != frame_start: time_opts = multiverse.TimeOptions() time_opts.writeTimeRange = True handle_start = instance.data['handleStart'] handle_end = instance.data['handleEnd'] time_opts.frameRange = ( frame_start - handle_start, frame_end + handle_end) time_opts.frameIncrement = instance.data['step'] time_opts.numTimeSamples = instance.data.get( 'numTimeSamples', options['numTimeSamples']) time_opts.timeSamplesSpan = instance.data.get( 'timeSamplesSpan', options['timeSamplesSpan']) time_opts.framePerSecond = instance.data.get( 'fps', mel.eval('currentTimeUnitToFPS()')) asset_write_opts = multiverse.AssetWriteOptions(time_opts) options_discard_keys = { 'numTimeSamples', 'timeSamplesSpan', 'frameStart', 'frameEnd', 'handleStart', 'handleEnd', 'step', 'fps' } self.log.debug("Write Options:") for key, value in options.items(): if key in options_discard_keys: continue self.log.debug(" - {}={}".format(key, value)) setattr(asset_write_opts, key, value) self.log.debug('WriteAsset: {} / {}'.format(file_path, members)) multiverse.WriteAsset(file_path, members, asset_write_opts) if "representations" not in instance.data: instance.data["representations"] = [] representation = { 'name': self.scene_type, 'ext': self.scene_type, 'files': file_name, 'stagingDir': staging_dir } instance.data["representations"].append(representation) self.log.debug("Extracted instance {} to {}".format( instance.name, file_path)) class ExtractMultiverseUsdAnim(ExtractMultiverseUsd): """Extractor for Multiverse USD Animation Sparse Cache data. This will extract the sparse cache data from the scene and generate a USD file with all the animation data. Upon publish a .usd sparse cache will be written. """ label = "Extract Multiverse USD Animation Sparse Cache" families = ["animation", "usd"] match = pyblish.api.Subset def get_default_options(self): anim_options = self.default_options anim_options["writeSparseOverrides"] = True anim_options["writeUsdAttributes"] = True anim_options["stripNamespaces"] = True return anim_options def filter_members(self, members): out_set = next((i for i in members if i.endswith("out_SET")), None) if out_set is None: self.log.warning("Expecting out_SET") return None members = cmds.ls(cmds.sets(out_set, query=True), long=True) return members ================================================ FILE: openpype/hosts/maya/plugins/publish/extract_multiverse_usd_comp.py ================================================ import os from maya import cmds from openpype.pipeline import publish from openpype.hosts.maya.api.lib import maintained_selection class ExtractMultiverseUsdComposition(publish.Extractor): """Extractor of Multiverse USD Composition data. This will extract settings for a Multiverse Write Composition operation: they are visible in the Maya set node created by a Multiverse USD Composition instance creator. The input data contained in the set is either: - a single hierarchy consisting of several Multiverse Compound nodes, with any number of layers, and Maya transform nodes - a single Compound node with more than one layer (in this case the "Write as Compound Layers" option should be set). Upon publish a .usda composition file will be written. """ label = "Extract Multiverse USD Composition" hosts = ["maya"] families = ["mvUsdComposition"] scene_type = "usd" # Order of `fileFormat` must match create_multiverse_usd_comp.py file_formats = ["usda", "usd"] @property def options(self): """Overridable options for Multiverse USD Export Given in the following format - {NAME: EXPECTED TYPE} If the overridden option's type does not match, the option is not included and a warning is logged. """ return { "stripNamespaces": bool, "mergeTransformAndShape": bool, "flattenContent": bool, "writeAsCompoundLayers": bool, "writePendingOverrides": bool, "numTimeSamples": int, "timeSamplesSpan": float } @property def default_options(self): """The default options for Multiverse USD extraction.""" return { "stripNamespaces": True, "mergeTransformAndShape": False, "flattenContent": False, "writeAsCompoundLayers": False, "writePendingOverrides": False, "numTimeSamples": 1, "timeSamplesSpan": 0.0 } def parse_overrides(self, instance, options): """Inspect data of instance to determine overridden options""" for key in instance.data: if key not in self.options: continue # Ensure the data is of correct type value = instance.data[key] if not isinstance(value, self.options[key]): self.log.warning( "Overridden attribute {key} was of " "the wrong type: {invalid_type} " "- should have been {valid_type}".format( key=key, invalid_type=type(value).__name__, valid_type=self.options[key].__name__)) continue options[key] = value return options def process(self, instance): # Load plugin first cmds.loadPlugin("MultiverseForMaya", quiet=True) # Define output file path staging_dir = self.staging_dir(instance) file_format = instance.data.get("fileFormat", 0) if file_format in range(len(self.file_formats)): self.scene_type = self.file_formats[file_format] file_name = "{0}.{1}".format(instance.name, self.scene_type) file_path = os.path.join(staging_dir, file_name) file_path = file_path.replace('\\', '/') # Parse export options options = self.default_options options = self.parse_overrides(instance, options) self.log.debug("Export options: {0}".format(options)) # Perform extraction self.log.debug("Performing extraction ...") with maintained_selection(): members = instance.data("setMembers") self.log.debug('Collected object {}'.format(members)) import multiverse time_opts = None frame_start = instance.data['frameStart'] frame_end = instance.data['frameEnd'] handle_start = instance.data['handleStart'] handle_end = instance.data['handleEnd'] step = instance.data['step'] fps = instance.data['fps'] if frame_end != frame_start: time_opts = multiverse.TimeOptions() time_opts.writeTimeRange = True time_opts.frameRange = ( frame_start - handle_start, frame_end + handle_end) time_opts.frameIncrement = step time_opts.numTimeSamples = instance.data["numTimeSamples"] time_opts.timeSamplesSpan = instance.data["timeSamplesSpan"] time_opts.framePerSecond = fps comp_write_opts = multiverse.CompositionWriteOptions() """ OP tells MV to write to a staging directory, and then moves the file to it's final publish directory. By default, MV write relative paths, but these paths will break when the referencing file moves. This option forces writes to absolute paths, which is ok within OP because all published assets have static paths, and MV can only reference published assets. When a proper UsdAssetResolver is used, this won't be needed. """ comp_write_opts.forceAbsolutePaths = True options_discard_keys = { 'numTimeSamples', 'timeSamplesSpan', 'frameStart', 'frameEnd', 'handleStart', 'handleEnd', 'step', 'fps' } for key, value in options.items(): if key in options_discard_keys: continue setattr(comp_write_opts, key, value) multiverse.WriteComposition(file_path, members, comp_write_opts) if "representations" not in instance.data: instance.data["representations"] = [] representation = { 'name': self.scene_type, 'ext': self.scene_type, 'files': file_name, 'stagingDir': staging_dir } instance.data["representations"].append(representation) self.log.debug("Extracted instance {} to {}".format(instance.name, file_path)) ================================================ FILE: openpype/hosts/maya/plugins/publish/extract_multiverse_usd_over.py ================================================ import os from openpype.pipeline import publish from openpype.hosts.maya.api.lib import maintained_selection from maya import cmds class ExtractMultiverseUsdOverride(publish.Extractor): """Extractor for Multiverse USD Override data. This will extract settings for a Multiverse Write Override operation: they are visible in the Maya set node created by a Multiverse USD Override instance creator. The input data contained in the set is: - a single Multiverse Compound node with any number of overrides (typically set in MEOW) Upon publish a .usda override file will be written. """ label = "Extract Multiverse USD Override" hosts = ["maya"] families = ["mvUsdOverride"] scene_type = "usd" # Order of `fileFormat` must match create_multiverse_usd_over.py file_formats = ["usda", "usd"] @property def options(self): """Overridable options for Multiverse USD Export Given in the following format - {NAME: EXPECTED TYPE} If the overridden option's type does not match, the option is not included and a warning is logged. """ return { "writeAll": bool, "writeTransforms": bool, "writeVisibility": bool, "writeAttributes": bool, "writeMaterials": bool, "writeVariants": bool, "writeVariantsDefinition": bool, "writeActiveState": bool, "writeNamespaces": bool, "numTimeSamples": int, "timeSamplesSpan": float } @property def default_options(self): """The default options for Multiverse USD extraction.""" return { "writeAll": False, "writeTransforms": True, "writeVisibility": True, "writeAttributes": True, "writeMaterials": True, "writeVariants": True, "writeVariantsDefinition": True, "writeActiveState": True, "writeNamespaces": False, "numTimeSamples": 1, "timeSamplesSpan": 0.0 } def process(self, instance): # Load plugin first cmds.loadPlugin("MultiverseForMaya", quiet=True) # Define output file path staging_dir = self.staging_dir(instance) file_format = instance.data.get("fileFormat", 0) if file_format in range(len(self.file_formats)): self.scene_type = self.file_formats[file_format] file_name = "{0}.{1}".format(instance.name, self.scene_type) file_path = os.path.join(staging_dir, file_name) file_path = file_path.replace("\\", "/") # Parse export options options = self.default_options self.log.debug("Export options: {0}".format(options)) # Perform extraction self.log.debug("Performing extraction ...") with maintained_selection(): members = instance.data("setMembers") members = cmds.ls(members, dag=True, shapes=False, type="mvUsdCompoundShape", noIntermediate=True, long=True) self.log.debug("Collected object {}".format(members)) # TODO: Deal with asset, composition, override with options. import multiverse time_opts = None frame_start = instance.data["frameStart"] frame_end = instance.data["frameEnd"] handle_start = instance.data["handleStart"] handle_end = instance.data["handleEnd"] step = instance.data["step"] fps = instance.data["fps"] if frame_end != frame_start: time_opts = multiverse.TimeOptions() time_opts.writeTimeRange = True time_opts.frameRange = ( frame_start - handle_start, frame_end + handle_end) time_opts.frameIncrement = step time_opts.numTimeSamples = instance.data["numTimeSamples"] time_opts.timeSamplesSpan = instance.data["timeSamplesSpan"] time_opts.framePerSecond = fps over_write_opts = multiverse.OverridesWriteOptions(time_opts) options_discard_keys = { "numTimeSamples", "timeSamplesSpan", "frameStart", "frameEnd", "handleStart", "handleEnd", "step", "fps" } for key, value in options.items(): if key in options_discard_keys: continue setattr(over_write_opts, key, value) for member in members: multiverse.WriteOverrides(file_path, member, over_write_opts) if "representations" not in instance.data: instance.data["representations"] = [] representation = { 'name': self.scene_type, 'ext': self.scene_type, 'files': file_name, 'stagingDir': staging_dir } instance.data["representations"].append(representation) self.log.debug("Extracted instance {} to {}".format( instance.name, file_path)) ================================================ FILE: openpype/hosts/maya/plugins/publish/extract_obj.py ================================================ # -*- coding: utf-8 -*- import os from maya import cmds import pyblish.api from openpype.pipeline import publish from openpype.hosts.maya.api import lib class ExtractObj(publish.Extractor): """Extract OBJ from Maya. This extracts reproducible OBJ exports ignoring any of the settings set on the local machine in the OBJ export options window. """ order = pyblish.api.ExtractorOrder hosts = ["maya"] label = "Extract OBJ" families = ["model"] def process(self, instance): # Define output path staging_dir = self.staging_dir(instance) filename = "{0}.obj".format(instance.name) path = os.path.join(staging_dir, filename) # The export requires forward slashes because we need to # format it into a string in a mel expression self.log.debug("Extracting OBJ to: {0}".format(path)) members = instance.data("setMembers") members = cmds.ls(members, dag=True, shapes=True, type=("mesh", "nurbsCurve"), noIntermediate=True, long=True) self.log.debug("Members: {0}".format(members)) self.log.debug("Instance: {0}".format(instance[:])) if not cmds.pluginInfo('objExport', query=True, loaded=True): cmds.loadPlugin('objExport') # Export with lib.no_display_layers(instance): with lib.displaySmoothness(members, divisionsU=0, divisionsV=0, pointsWire=4, pointsShaded=1, polygonObject=1): with lib.shader(members, shadingEngine="initialShadingGroup"): with lib.maintained_selection(): cmds.select(members, noExpand=True) cmds.file(path, exportSelected=True, type='OBJexport', preserveReferences=True, force=True) if "representation" not in instance.data: instance.data["representation"] = [] representation = { 'name': 'obj', 'ext': 'obj', 'files': filename, "stagingDir": staging_dir, } instance.data["representations"].append(representation) self.log.debug("Extract OBJ successful to: {0}".format(path)) ================================================ FILE: openpype/hosts/maya/plugins/publish/extract_playblast.py ================================================ import os import clique from openpype.pipeline import publish from openpype.hosts.maya.api import lib from maya import cmds class ExtractPlayblast(publish.Extractor): """Extract viewport playblast. Takes review camera and creates review Quicktime video based on viewport capture. """ label = "Extract Playblast" hosts = ["maya"] families = ["review"] optional = True capture_preset = {} profiles = None def process(self, instance): self.log.debug("Extracting playblast..") # get scene fps fps = instance.data.get("fps") or instance.context.data.get("fps") # if start and end frames cannot be determined, get them # from Maya timeline start = instance.data.get("frameStartFtrack") end = instance.data.get("frameEndFtrack") if start is None: start = cmds.playbackOptions(query=True, animationStartTime=True) if end is None: end = cmds.playbackOptions(query=True, animationEndTime=True) self.log.debug("start: {}, end: {}".format(start, end)) task_data = instance.data["anatomyData"].get("task", {}) capture_preset = lib.get_capture_preset( task_data.get("name"), task_data.get("type"), instance.data["subset"], instance.context.data["project_settings"], self.log ) stagingdir = self.staging_dir(instance) filename = instance.name path = os.path.join(stagingdir, filename) self.log.debug("Outputting images to %s" % path) # get cameras camera = instance.data["review_camera"] preset = lib.generate_capture_preset( instance, camera, path, start=start, end=end, capture_preset=capture_preset) lib.render_capture_preset(preset) # Find playblast sequence collected_files = os.listdir(stagingdir) patterns = [clique.PATTERNS["frames"]] collections, remainder = clique.assemble(collected_files, minimum_items=1, patterns=patterns) self.log.debug("Searching playblast collection for: %s", path) frame_collection = None for collection in collections: filebase = collection.format("{head}").rstrip(".") self.log.debug("Checking collection head: %s", filebase) if filebase in path: frame_collection = collection self.log.debug( "Found playblast collection: %s", frame_collection ) tags = ["review"] if not instance.data.get("keepImages"): tags.append("delete") # Add camera node name to representation data camera_node_name = cmds.listRelatives(camera, parent=True)[0] collected_files = list(frame_collection) # single frame file shouldn't be in list, only as a string if len(collected_files) == 1: collected_files = collected_files[0] if "representations" not in instance.data: instance.data["representations"] = [] representation = { "name": capture_preset["Codec"]["compression"], "ext": capture_preset["Codec"]["compression"], "files": collected_files, "stagingDir": stagingdir, "frameStart": int(start), "frameEnd": int(end), "fps": fps, "tags": tags, "camera_name": camera_node_name } instance.data["representations"].append(representation) ================================================ FILE: openpype/hosts/maya/plugins/publish/extract_pointcache.py ================================================ import os from maya import cmds from openpype.pipeline import publish from openpype.hosts.maya.api.alembic import extract_alembic from openpype.hosts.maya.api.lib import ( suspended_refresh, maintained_selection, iter_visible_nodes_in_range, ) from openpype.lib import ( BoolDef, TextDef, NumberDef, EnumDef, UISeparatorDef, UILabelDef, ) from openpype.pipeline.publish import OpenPypePyblishPluginMixin class ExtractAlembic(publish.Extractor, OpenPypePyblishPluginMixin): """Produce an alembic of just point positions and normals. Positions and normals, uvs, creases are preserved, but nothing more, for plain and predictable point caches. Plugin can run locally or remotely (on a farm - if instance is marked with "farm" it will be skipped in local processing, but processed on farm) """ label = "Extract Pointcache (Alembic)" hosts = ["maya"] families = ["pointcache", "model", "vrayproxy.alembic"] targets = ["local", "remote"] flags = [] attr = [] attrPrefix = [] dataFormat = "ogawa" melPerFrameCallback = "" melPostJobCallback = "" preRollStartFrame = 0 pythonPerFrameCallback = "" pythonPostJobCallback = "" userAttr = "" userAttrPrefix = "" visibleOnly = False overrides = [] def process(self, instance): if instance.data.get("farm"): self.log.debug("Should be processed on farm, skipping.") return nodes, roots = self.get_members_and_roots(instance) # Collect the start and end including handles start = float(instance.data.get("frameStartHandle", 1)) end = float(instance.data.get("frameEndHandle", 1)) attribute_values = self.get_attr_values_from_data( instance.data ) attrs = [ attr.strip() for attr in attribute_values.get("attr", "").split(";") if attr.strip() ] attrs += instance.data.get("userDefinedAttributes", []) attrs += ["cbId"] attr_prefixes = [ attr.strip() for attr in attribute_values.get("attrPrefix", "").split(";") if attr.strip() ] self.log.debug("Extracting pointcache...") dirname = self.staging_dir(instance) parent_dir = self.staging_dir(instance) filename = "{name}.abc".format(**instance.data) path = os.path.join(parent_dir, filename) root = None if not instance.data.get("includeParentHierarchy", True): # Set the root nodes if we don't want to include parents # The roots are to be considered the ones that are the actual # direct members of the set root = roots args = { "file": path, "attr": attrs, "attrPrefix": attr_prefixes, "dataFormat": attribute_values.get("dataFormat", "ogawa"), "endFrame": end, "eulerFilter": False, "noNormals": False, "preRoll": False, "preRollStartFrame": attribute_values.get( "preRollStartFrame", 0 ), "renderableOnly": False, "root": root, "selection": True, "startFrame": start, "step": instance.data.get( "creator_attributes", {} ).get("step", 1.0), "stripNamespaces": False, "uvWrite": False, "verbose": False, "wholeFrameGeo": False, "worldSpace": False, "writeColorSets": False, "writeCreases": False, "writeFaceSets": False, "writeUVSets": False, "writeVisibility": False, } # Export flags are defined as default enabled flags plus publisher # enabled flags. non_exposed_flags = list(set(self.flags) - set(self.overrides)) flags = attribute_values["flags"] + non_exposed_flags for flag in flags: args[flag] = True if instance.data.get("visibleOnly", False): # If we only want to include nodes that are visible in the frame # range then we need to do our own check. Alembic's `visibleOnly` # flag does not filter out those that are only hidden on some # frames as it counts "animated" or "connected" visibilities as # if it's always visible. nodes = list( iter_visible_nodes_in_range(nodes, start=start, end=end) ) suspend = not instance.data.get("refresh", False) with suspended_refresh(suspend=suspend): with maintained_selection(): cmds.select(nodes, noExpand=True) self.log.debug( "Running `extract_alembic` with the arguments: {}".format( args ) ) extract_alembic(**args) if "representations" not in instance.data: instance.data["representations"] = [] representation = { "name": "abc", "ext": "abc", "files": filename, "stagingDir": dirname, } instance.data["representations"].append(representation) if not instance.data.get("stagingDir_persistent", False): instance.context.data["cleanupFullPaths"].append(path) self.log.debug("Extracted {} to {}".format(instance, dirname)) # Extract proxy. if not instance.data.get("proxy"): self.log.debug("No proxy nodes found. Skipping proxy extraction.") return path = path.replace(".abc", "_proxy.abc") args["file"] = path if not instance.data.get("includeParentHierarchy", True): # Set the root nodes if we don't want to include parents # The roots are to be considered the ones that are the actual # direct members of the set args["root"] = instance.data["proxyRoots"] with suspended_refresh(suspend=suspend): with maintained_selection(): cmds.select(instance.data["proxy"]) extract_alembic(**args) representation = { "name": "proxy", "ext": "abc", "files": os.path.basename(path), "stagingDir": dirname, "outputName": "proxy", } instance.data["representations"].append(representation) def get_members_and_roots(self, instance): return instance[:], instance.data.get("setMembers") @classmethod def get_attribute_defs(cls): override_defs = { "attr": { "def": TextDef, "kwargs": { "label": "Custom Attributes", "placeholder": "attr1; attr2; ...", } }, "attrPrefix": { "def": TextDef, "kwargs": { "label": "Custom Attributes Prefix", "placeholder": "prefix1; prefix2; ...", } }, "dataFormat": { "def": EnumDef, "kwargs": { "label": "Data Format", "items": ["ogawa", "HDF"], } }, "melPerFrameCallback": { "def": TextDef, "kwargs": { "label": "melPerFrameCallback", } }, "melPostJobCallback": { "def": TextDef, "kwargs": { "label": "melPostJobCallback", } }, "preRollStartFrame": { "def": NumberDef, "kwargs": { "label": "Start frame for preroll", "tooltip": ( "The frame to start scene evaluation at. This is used" " to set the starting frame for time dependent " "translations and can be used to evaluate run-up that" " isn't actually translated." ), } }, "pythonPerFrameCallback": { "def": TextDef, "kwargs": { "label": "pythonPerFrameCallback", } }, "pythonPostJobCallback": { "def": TextDef, "kwargs": { "label": "pythonPostJobCallback", } }, "userAttr": { "def": TextDef, "kwargs": { "label": "userAttr", } }, "userAttrPrefix": { "def": TextDef, "kwargs": { "label": "userAttrPrefix", } }, "visibleOnly": { "def": BoolDef, "kwargs": { "label": "Visible Only", } } } defs = super(ExtractAlembic, cls).get_attribute_defs() defs.extend([ UISeparatorDef("sep_alembic_options"), UILabelDef("Alembic Options"), ]) # The Arguments that can be modified by the Publisher overrides = set(getattr(cls, "overrides", set())) # What we have set in the Settings as defaults. flags = set(getattr(cls, "flags", set())) enabled_flags = [x for x in flags if x in overrides] flags = overrides - set(override_defs.keys()) if flags: defs.append( EnumDef( "flags", flags, default=enabled_flags, multiselection=True, label="Export Flags", ) ) for key, value in override_defs.items(): if key not in overrides: continue kwargs = value["kwargs"] kwargs["default"] = getattr(cls, key, None) defs.append( value["def"](key, **value["kwargs"]) ) defs.append( UISeparatorDef("sep_alembic_options") ) return defs class ExtractAnimation(ExtractAlembic): label = "Extract Animation (Alembic)" families = ["animation"] def get_members_and_roots(self, instance): # Collect the out set nodes out_sets = [node for node in instance if node.endswith("out_SET")] if len(out_sets) != 1: raise RuntimeError( "Couldn't find exactly one out_SET: " "{0}".format(out_sets) ) out_set = out_sets[0] roots = cmds.sets(out_set, query=True) # Include all descendants nodes = ( roots + cmds.listRelatives(roots, allDescendents=True, fullPath=True) or [] ) return nodes, roots ================================================ FILE: openpype/hosts/maya/plugins/publish/extract_proxy_abc.py ================================================ import os from maya import cmds from openpype.pipeline import publish from openpype.hosts.maya.api.alembic import extract_alembic from openpype.hosts.maya.api.lib import ( suspended_refresh, maintained_selection, iter_visible_nodes_in_range ) class ExtractProxyAlembic(publish.Extractor): """Produce an alembic for bounding box geometry """ label = "Extract Proxy (Alembic)" hosts = ["maya"] families = ["proxyAbc"] def process(self, instance): name_suffix = instance.data.get("nameSuffix") # Collect the start and end including handles start = float(instance.data.get("frameStartHandle", 1)) end = float(instance.data.get("frameEndHandle", 1)) attrs = instance.data.get("attr", "").split(";") attrs = [value for value in attrs if value.strip()] attrs += ["cbId"] attr_prefixes = instance.data.get("attrPrefix", "").split(";") attr_prefixes = [value for value in attr_prefixes if value.strip()] self.log.debug("Extracting Proxy Alembic..") dirname = self.staging_dir(instance) filename = "{name}.abc".format(**instance.data) path = os.path.join(dirname, filename) proxy_root = self.create_proxy_geometry(instance, name_suffix, start, end) options = { "step": instance.data.get("step", 1.0), "attr": attrs, "attrPrefix": attr_prefixes, "writeVisibility": True, "writeCreases": True, "writeColorSets": instance.data.get("writeColorSets", False), "writeFaceSets": instance.data.get("writeFaceSets", False), "uvWrite": True, "selection": True, "worldSpace": instance.data.get("worldSpace", True), "root": proxy_root } if int(cmds.about(version=True)) >= 2017: # Since Maya 2017 alembic supports multiple uv sets - write them. options["writeUVSets"] = True with suspended_refresh(): with maintained_selection(): cmds.select(proxy_root, hi=True, noExpand=True) extract_alembic(file=path, startFrame=start, endFrame=end, **options) if "representations" not in instance.data: instance.data["representations"] = [] representation = { 'name': 'abc', 'ext': 'abc', 'files': filename, "stagingDir": dirname } instance.data["representations"].append(representation) if not instance.data.get("stagingDir_persistent", False): instance.context.data["cleanupFullPaths"].append(path) self.log.debug("Extracted {} to {}".format(instance, dirname)) # remove the bounding box bbox_master = cmds.ls("bbox_grp") cmds.delete(bbox_master) def create_proxy_geometry(self, instance, name_suffix, start, end): nodes = instance[:] nodes = list(iter_visible_nodes_in_range(nodes, start=start, end=end)) inst_selection = cmds.ls(nodes, long=True) cmds.geomToBBox(inst_selection, nameSuffix=name_suffix, keepOriginal=True, single=False, bakeAnimation=True, startTime=start, endTime=end) # create master group for bounding # boxes as the main root master_group = cmds.group(name="bbox_grp") bbox_sel = cmds.ls(master_group, long=True) self.log.debug("proxy_root: {}".format(bbox_sel)) return bbox_sel ================================================ FILE: openpype/hosts/maya/plugins/publish/extract_redshift_proxy.py ================================================ # -*- coding: utf-8 -*- """Redshift Proxy extractor.""" import os from maya import cmds from openpype.pipeline import publish from openpype.hosts.maya.api.lib import maintained_selection class ExtractRedshiftProxy(publish.Extractor): """Extract the content of the instance to a redshift proxy file.""" label = "Redshift Proxy (.rs)" hosts = ["maya"] families = ["redshiftproxy"] def process(self, instance): """Extractor entry point.""" staging_dir = self.staging_dir(instance) file_name = "{}.rs".format(instance.name) file_path = os.path.join(staging_dir, file_name) anim_on = instance.data["animation"] rs_options = "exportConnectivity=0;enableCompression=1;keepUnused=0;" repr_files = file_name if not anim_on: # Remove animation information because it is not required for # non-animated subsets keys = ["frameStart", "frameEnd", "handleStart", "handleEnd", "frameStartHandle", "frameEndHandle"] for key in keys: instance.data.pop(key, None) else: start_frame = instance.data["frameStartHandle"] end_frame = instance.data["frameEndHandle"] rs_options = "{}startFrame={};endFrame={};frameStep={};".format( rs_options, start_frame, end_frame, instance.data["step"] ) root, ext = os.path.splitext(file_path) # Padding is taken from number of digits of the end_frame. # Not sure where Redshift is taking it. repr_files = [ "{}.{}{}".format(os.path.basename(root), str(frame).rjust(4, "0"), ext) # noqa: E501 for frame in range( int(start_frame), int(end_frame) + 1, int(instance.data["step"]) )] # vertex_colors = instance.data.get("vertexColors", False) # Write out rs file self.log.debug("Writing: '%s'" % file_path) with maintained_selection(): cmds.select(instance.data["setMembers"], noExpand=True) cmds.file(file_path, pr=False, force=True, type="Redshift Proxy", exportSelected=True, options=rs_options) if "representations" not in instance.data: instance.data["representations"] = [] self.log.debug("Files: {}".format(repr_files)) representation = { 'name': 'rs', 'ext': 'rs', 'files': repr_files, "stagingDir": staging_dir, } instance.data["representations"].append(representation) self.log.debug("Extracted instance '%s' to: %s" % (instance.name, staging_dir)) ================================================ FILE: openpype/hosts/maya/plugins/publish/extract_rendersetup.py ================================================ import os import json import maya.app.renderSetup.model.renderSetup as renderSetup from openpype.pipeline import publish class ExtractRenderSetup(publish.Extractor): """ Produce renderSetup template file This will save whole renderSetup to json file for later use. """ label = "Extract RenderSetup" hosts = ["maya"] families = ["rendersetup"] def process(self, instance): parent_dir = self.staging_dir(instance) json_filename = "{}.json".format(instance.name) json_path = os.path.join(parent_dir, json_filename) with open(json_path, "w+") as file: json.dump( renderSetup.instance().encode(None), fp=file, indent=2, sort_keys=True) if "representations" not in instance.data: instance.data["representations"] = [] representation = { 'name': 'json', 'ext': 'json', 'files': json_filename, "stagingDir": parent_dir, } instance.data["representations"].append(representation) self.log.debug( "Extracted instance '%s' to: %s" % (instance.name, json_path)) ================================================ FILE: openpype/hosts/maya/plugins/publish/extract_rig.py ================================================ # -*- coding: utf-8 -*- """Extract rig as Maya Scene.""" import os from maya import cmds from openpype.pipeline import publish from openpype.hosts.maya.api.lib import maintained_selection class ExtractRig(publish.Extractor): """Extract rig as Maya Scene.""" label = "Extract Rig (Maya Scene)" hosts = ["maya"] families = ["rig"] scene_type = "ma" def process(self, instance): """Plugin entry point.""" ext_mapping = ( instance.context.data["project_settings"]["maya"]["ext_mapping"] ) if ext_mapping: self.log.debug("Looking in settings for scene type ...") # use extension mapping for first family found for family in self.families: try: self.scene_type = ext_mapping[family] self.log.debug( "Using '.{}' as scene type".format(self.scene_type)) break except AttributeError: # no preset found pass # Define extract output file path dir_path = self.staging_dir(instance) filename = "{0}.{1}".format(instance.name, self.scene_type) path = os.path.join(dir_path, filename) # Perform extraction self.log.debug("Performing extraction ...") with maintained_selection(): cmds.select(instance, noExpand=True) cmds.file(path, force=True, typ="mayaAscii" if self.scene_type == "ma" else "mayaBinary", # noqa: E501 exportSelected=True, preserveReferences=False, channels=True, constraints=True, expressions=True, constructionHistory=True) if "representations" not in instance.data: instance.data["representations"] = [] representation = { 'name': self.scene_type, 'ext': self.scene_type, 'files': filename, "stagingDir": dir_path } instance.data["representations"].append(representation) self.log.debug("Extracted instance '%s' to: %s", instance.name, path) ================================================ FILE: openpype/hosts/maya/plugins/publish/extract_skeleton_mesh.py ================================================ # -*- coding: utf-8 -*- import os from maya import cmds # noqa import pyblish.api from openpype.pipeline import publish from openpype.pipeline.publish import OptionalPyblishPluginMixin from openpype.hosts.maya.api import fbx class ExtractSkeletonMesh(publish.Extractor, OptionalPyblishPluginMixin): """Extract Rig in FBX format from Maya. This extracts the rig in fbx with the constraints and referenced asset content included. This also optionally extract animated rig in fbx with geometries included. """ order = pyblish.api.ExtractorOrder label = "Extract Skeleton Mesh" hosts = ["maya"] families = ["rig.fbx"] def process(self, instance): if not self.is_active(instance.data): return # Define output path staging_dir = self.staging_dir(instance) filename = "{0}.fbx".format(instance.name) path = os.path.join(staging_dir, filename) fbx_exporter = fbx.FBXExtractor(log=self.log) out_set = instance.data.get("skeleton_mesh", []) instance.data["constraints"] = True instance.data["skeletonDefinitions"] = True fbx_exporter.set_options_from_instance(instance) # Export fbx_exporter.export(out_set, path) representations = instance.data.setdefault("representations", []) representations.append({ 'name': 'fbx', 'ext': 'fbx', 'files': filename, "stagingDir": staging_dir }) self.log.debug("Extract FBX to: {0}".format(path)) ================================================ FILE: openpype/hosts/maya/plugins/publish/extract_thumbnail.py ================================================ import os import glob import tempfile from openpype.pipeline import publish from openpype.hosts.maya.api import lib class ExtractThumbnail(publish.Extractor): """Extract viewport thumbnail. Takes review camera and creates a thumbnail based on viewport capture. """ label = "Thumbnail" hosts = ["maya"] families = ["review"] def process(self, instance): self.log.debug("Extracting thumbnail..") camera = instance.data["review_camera"] task_data = instance.data["anatomyData"].get("task", {}) capture_preset = lib.get_capture_preset( task_data.get("name"), task_data.get("type"), instance.data["subset"], instance.context.data["project_settings"], self.log ) # Create temp directory for thumbnail # - this is to avoid "override" of source file dst_staging = tempfile.mkdtemp(prefix="pyblish_tmp_thumbnail") self.log.debug( "Create temp directory {} for thumbnail".format(dst_staging) ) # Store new staging to cleanup paths filename = instance.name path = os.path.join(dst_staging, filename) self.log.debug("Outputting images to %s" % path) preset = lib.generate_capture_preset( instance, camera, path, start=1, end=1, capture_preset=capture_preset) preset["camera_options"].update({ "displayGateMask": False, "displayResolution": False, "displayFilmGate": False, "displayFieldChart": False, "displaySafeAction": False, "displaySafeTitle": False, "displayFilmPivot": False, "displayFilmOrigin": False, "overscan": 1.0, }) path = lib.render_capture_preset(preset) playblast = self._fix_playblast_output_path(path) _, thumbnail = os.path.split(playblast) self.log.debug("file list {}".format(thumbnail)) if "representations" not in instance.data: instance.data["representations"] = [] representation = { "name": "thumbnail", "ext": "jpg", "files": thumbnail, "stagingDir": dst_staging, "thumbnail": True } instance.data["representations"].append(representation) def _fix_playblast_output_path(self, filepath): """Workaround a bug in maya.cmds.playblast to return correct filepath. When the `viewer` argument is set to False and maya.cmds.playblast does not automatically open the playblasted file the returned filepath does not have the file's extension added correctly. To workaround this we just glob.glob() for any file extensions and assume the latest modified file is the correct file and return it. """ # Catch cancelled playblast if filepath is None: self.log.warning("Playblast did not result in output path. " "Playblast is probably interrupted.") return None # Fix: playblast not returning correct filename (with extension) # Lets assume the most recently modified file is the correct one. if not os.path.exists(filepath): directory = os.path.dirname(filepath) filename = os.path.basename(filepath) # check if the filepath is has frame based filename # example : capture.####.png parts = filename.split(".") if len(parts) == 3: query = os.path.join(directory, "{}.*.{}".format(parts[0], parts[-1])) files = glob.glob(query) else: files = glob.glob("{}.*".format(filepath)) if not files: raise RuntimeError("Couldn't find playblast from: " "{0}".format(filepath)) filepath = max(files, key=os.path.getmtime) return filepath ================================================ FILE: openpype/hosts/maya/plugins/publish/extract_unreal_skeletalmesh_abc.py ================================================ # -*- coding: utf-8 -*- """Create Unreal Skeletal Mesh data to be extracted as FBX.""" import os from contextlib import contextmanager from maya import cmds # noqa from openpype.pipeline import publish from openpype.hosts.maya.api.alembic import extract_alembic from openpype.hosts.maya.api.lib import ( suspended_refresh, maintained_selection ) class ExtractUnrealSkeletalMeshAbc(publish.Extractor): """Extract Unreal Skeletal Mesh as FBX from Maya. """ label = "Extract Unreal Skeletal Mesh - Alembic" hosts = ["maya"] families = ["skeletalMesh"] optional = True def process(self, instance): self.log.debug("Extracting pointcache..") geo = cmds.listRelatives( instance.data.get("geometry"), allDescendents=True, fullPath=True) joints = cmds.listRelatives( instance.data.get("joints"), allDescendents=True, fullPath=True) nodes = geo + joints attrs = instance.data.get("attr", "").split(";") attrs = [value for value in attrs if value.strip()] attrs += ["cbId"] attr_prefixes = instance.data.get("attrPrefix", "").split(";") attr_prefixes = [value for value in attr_prefixes if value.strip()] # Define output path staging_dir = self.staging_dir(instance) filename = "{0}.abc".format(instance.name) path = os.path.join(staging_dir, filename) # The export requires forward slashes because we need # to format it into a string in a mel expression path = path.replace('\\', '/') self.log.debug("Extracting ABC to: {0}".format(path)) self.log.debug("Members: {0}".format(nodes)) self.log.debug("Instance: {0}".format(instance[:])) options = { "step": instance.data.get("step", 1.0), "attr": attrs, "attrPrefix": attr_prefixes, "writeVisibility": True, "writeCreases": True, "writeColorSets": instance.data.get("writeColorSets", False), "writeFaceSets": instance.data.get("writeFaceSets", False), "uvWrite": True, "selection": True, "worldSpace": instance.data.get("worldSpace", True) } self.log.debug("Options: {}".format(options)) if int(cmds.about(version=True)) >= 2017: # Since Maya 2017 alembic supports multiple uv sets - write them. options["writeUVSets"] = True if not instance.data.get("includeParentHierarchy", True): # Set the root nodes if we don't want to include parents # The roots are to be considered the ones that are the actual # direct members of the set options["root"] = instance.data.get("setMembers") with suspended_refresh(suspend=instance.data.get("refresh", False)): with maintained_selection(): cmds.select(nodes, noExpand=True) extract_alembic(file=path, # startFrame=start, # endFrame=end, **options) if "representations" not in instance.data: instance.data["representations"] = [] representation = { 'name': 'abc', 'ext': 'abc', 'files': filename, "stagingDir": staging_dir, } instance.data["representations"].append(representation) self.log.debug("Extract ABC successful to: {0}".format(path)) ================================================ FILE: openpype/hosts/maya/plugins/publish/extract_unreal_skeletalmesh_fbx.py ================================================ # -*- coding: utf-8 -*- """Create Unreal Skeletal Mesh data to be extracted as FBX.""" import os from contextlib import contextmanager from maya import cmds # noqa import pyblish.api from openpype.pipeline import publish from openpype.hosts.maya.api import fbx @contextmanager def renamed(original_name, renamed_name): # type: (str, str) -> None try: cmds.rename(original_name, renamed_name) yield finally: cmds.rename(renamed_name, original_name) class ExtractUnrealSkeletalMeshFbx(publish.Extractor): """Extract Unreal Skeletal Mesh as FBX from Maya. """ order = pyblish.api.ExtractorOrder - 0.1 label = "Extract Unreal Skeletal Mesh - FBX" families = ["skeletalMesh"] optional = True def process(self, instance): fbx_exporter = fbx.FBXExtractor(log=self.log) # Define output path staging_dir = self.staging_dir(instance) filename = "{0}.fbx".format(instance.name) path = os.path.join(staging_dir, filename) geo = instance.data.get("geometry") joints = instance.data.get("joints") to_extract = geo + joints # The export requires forward slashes because we need # to format it into a string in a mel expression path = path.replace('\\', '/') self.log.debug("Extracting FBX to: {0}".format(path)) self.log.debug("Members: {0}".format(to_extract)) self.log.debug("Instance: {0}".format(instance[:])) fbx_exporter.set_options_from_instance(instance) # This magic is done for variants. To let Unreal merge correctly # existing data, top node must have the same name. So for every # variant we extract we need to rename top node of the rig correctly. # It is finally done in context manager so it won't affect current # scene. # we rely on hierarchy under one root. original_parent = to_extract[0].split("|")[1] parent_node = instance.data.get("asset") # this needs to be done for AYON # WARNING: since AYON supports duplicity of asset names, # this needs to be refactored throughout the pipeline. parent_node = parent_node.split("/")[-1] renamed_to_extract = [] for node in to_extract: node_path = node.split("|") node_path[1] = parent_node renamed_to_extract.append("|".join(node_path)) with renamed(original_parent, parent_node): self.log.debug("Extracting: {}".format(renamed_to_extract, path)) fbx_exporter.export(renamed_to_extract, path) if "representations" not in instance.data: instance.data["representations"] = [] representation = { 'name': 'fbx', 'ext': 'fbx', 'files': filename, "stagingDir": staging_dir, } instance.data["representations"].append(representation) self.log.debug("Extract FBX successful to: {0}".format(path)) ================================================ FILE: openpype/hosts/maya/plugins/publish/extract_unreal_staticmesh.py ================================================ # -*- coding: utf-8 -*- """Create Unreal Static Mesh data to be extracted as FBX.""" import os from maya import cmds # noqa import pyblish.api from openpype.pipeline import publish from openpype.hosts.maya.api.lib import ( parent_nodes, maintained_selection ) from openpype.hosts.maya.api import fbx class ExtractUnrealStaticMesh(publish.Extractor): """Extract Unreal Static Mesh as FBX from Maya. """ order = pyblish.api.ExtractorOrder - 0.1 label = "Extract Unreal Static Mesh" families = ["staticMesh"] def process(self, instance): members = instance.data.get("geometryMembers", []) if instance.data.get("collisionMembers"): members = members + instance.data.get("collisionMembers") fbx_exporter = fbx.FBXExtractor(log=self.log) # Define output path staging_dir = self.staging_dir(instance) filename = "{0}.fbx".format(instance.name) path = os.path.join(staging_dir, filename) # The export requires forward slashes because we need # to format it into a string in a mel expression path = path.replace('\\', '/') self.log.debug("Extracting FBX to: {0}".format(path)) self.log.debug("Members: {0}".format(members)) self.log.debug("Instance: {0}".format(instance[:])) fbx_exporter.set_options_from_instance(instance) with maintained_selection(): with parent_nodes(members): self.log.debug("Un-parenting: {}".format(members)) fbx_exporter.export(members, path) if "representations" not in instance.data: instance.data["representations"] = [] representation = { 'name': 'fbx', 'ext': 'fbx', 'files': filename, "stagingDir": staging_dir, } instance.data["representations"].append(representation) self.log.debug("Extract FBX successful to: {0}".format(path)) ================================================ FILE: openpype/hosts/maya/plugins/publish/extract_unreal_yeticache.py ================================================ import os from maya import cmds from openpype.pipeline import publish class ExtractUnrealYetiCache(publish.Extractor): """Producing Yeti cache files using scene time range. This will extract Yeti cache file sequence and fur settings. """ label = "Extract Yeti Cache (Unreal)" hosts = ["maya"] families = ["yeticacheUE"] def process(self, instance): yeti_nodes = cmds.ls(instance, type="pgYetiMaya") if not yeti_nodes: raise RuntimeError("No pgYetiMaya nodes found in the instance") # Define extract output file path dirname = self.staging_dir(instance) # Collect information for writing cache start_frame = instance.data["frameStartHandle"] end_frame = instance.data["frameEndHandle"] preroll = instance.data["preroll"] if preroll > 0: start_frame -= preroll kwargs = {} samples = instance.data.get("samples", 0) if samples == 0: kwargs.update({"sampleTimes": "0.0 1.0"}) else: kwargs.update({"samples": samples}) self.log.debug(f"Writing out cache {start_frame} - {end_frame}") filename = f"{instance.name}.abc" path = os.path.join(dirname, filename) cmds.pgYetiCommand(yeti_nodes, writeAlembic=path, range=(start_frame, end_frame), asUnrealAbc=True, **kwargs) if "representations" not in instance.data: instance.data["representations"] = [] representation = { 'name': 'abc', 'ext': 'abc', 'files': filename, 'stagingDir': dirname } instance.data["representations"].append(representation) self.log.debug(f"Extracted {instance} to {dirname}") ================================================ FILE: openpype/hosts/maya/plugins/publish/extract_vrayproxy.py ================================================ import os from maya import cmds from openpype.pipeline import publish from openpype.hosts.maya.api.lib import maintained_selection class ExtractVRayProxy(publish.Extractor): """Extract the content of the instance to a vrmesh file Things to pay attention to: - If animation is toggled, are the frames correct - """ label = "VRay Proxy (.vrmesh)" hosts = ["maya"] families = ["vrayproxy.vrmesh"] def process(self, instance): staging_dir = self.staging_dir(instance) file_name = "{}.vrmesh".format(instance.name) file_path = os.path.join(staging_dir, file_name) anim_on = instance.data["animation"] if not anim_on: # Remove animation information because it is not required for # non-animated subsets keys = ["frameStart", "frameEnd", "handleStart", "handleEnd", "frameStartHandle", "frameEndHandle"] for key in keys: instance.data.pop(key, None) start_frame = 1 end_frame = 1 else: start_frame = instance.data["frameStartHandle"] end_frame = instance.data["frameEndHandle"] vertex_colors = instance.data.get("vertexColors", False) # Write out vrmesh file self.log.debug("Writing: '%s'" % file_path) with maintained_selection(): cmds.select(instance.data["setMembers"], noExpand=True) cmds.vrayCreateProxy(exportType=1, dir=staging_dir, fname=file_name, animOn=anim_on, animType=3, startFrame=start_frame, endFrame=end_frame, vertexColorsOn=vertex_colors, ignoreHiddenObjects=True, createProxyNode=False) if "representations" not in instance.data: instance.data["representations"] = [] representation = { 'name': 'vrmesh', 'ext': 'vrmesh', 'files': file_name, "stagingDir": staging_dir, } instance.data["representations"].append(representation) self.log.debug("Extracted instance '%s' to: %s" % (instance.name, staging_dir)) ================================================ FILE: openpype/hosts/maya/plugins/publish/extract_vrayscene.py ================================================ # -*- coding: utf-8 -*- """Extract vrayscene from specified families.""" import os import re from openpype.pipeline import publish from openpype.hosts.maya.api.render_setup_tools import export_in_rs_layer from openpype.hosts.maya.api.lib import maintained_selection from maya import cmds class ExtractVrayscene(publish.Extractor): """Extractor for vrscene.""" label = "VRay Scene (.vrscene)" hosts = ["maya"] families = ["vrayscene_layer"] def process(self, instance): """Plugin entry point.""" if instance.data.get("exportOnFarm"): self.log.debug("vrayscenes will be exported on farm.") raise NotImplementedError( "exporting vrayscenes is not implemented") # handle sequence if instance.data.get("vraySceneMultipleFiles"): self.log.debug("vrayscenes will be exported on farm.") raise NotImplementedError( "exporting vrayscene sequences not implemented yet") vray_settings = cmds.ls(type="VRaySettingsNode") if not vray_settings: node = cmds.createNode("VRaySettingsNode") else: node = vray_settings[0] # setMembers on vrayscene_layer should contain layer name. layer_name = instance.data.get("layer") staging_dir = self.staging_dir(instance) template = cmds.getAttr("{}.vrscene_filename".format(node)) start_frame = instance.data.get( "frameStartHandle") if instance.data.get( "vraySceneMultipleFiles") else None formatted_name = self.format_vray_output_filename( os.path.basename(instance.data.get("source")), layer_name, template, start_frame ) file_path = os.path.join( staging_dir, "vrayscene", *formatted_name.split("/")) # Write out vrscene file self.log.debug("Writing: '%s'" % file_path) with maintained_selection(): if "*" not in instance.data["setMembers"]: self.log.debug( "Exporting: {}".format(instance.data["setMembers"])) set_members = instance.data["setMembers"] cmds.select(set_members, noExpand=True) else: self.log.debug("Exporting all ...") set_members = cmds.ls( long=True, objectsOnly=True, geometry=True, lights=True, cameras=True) cmds.select(set_members, noExpand=True) self.log.debug("Appending layer name {}".format(layer_name)) set_members.append(layer_name) export_in_rs_layer( file_path, set_members, export=lambda: cmds.file( file_path, type="V-Ray Scene", pr=True, es=True, force=True)) if "representations" not in instance.data: instance.data["representations"] = [] files = file_path representation = { 'name': 'vrscene', 'ext': 'vrscene', 'files': os.path.basename(files), "stagingDir": os.path.dirname(files), } instance.data["representations"].append(representation) self.log.debug("Extracted instance '%s' to: %s" % (instance.name, staging_dir)) @staticmethod def format_vray_output_filename( filename, layer, template, start_frame=None): """Format the expected output file of the Export job. Example: filename: /mnt/projects/foo/shot010_v006.mb template: / / result: "shot010_v006/CHARS/CHARS.vrscene" Args: filename (str): path to scene file. layer (str): layer name. template (str): token template. start_frame (int, optional): start frame - if set we use multiple files export mode. Returns: str: formatted path. """ # format template to match pythons format specs template = re.sub(r"<(\w+?)>", r"{\1}", template.lower()) # Ensure filename has no extension file_name, _ = os.path.splitext(filename) mapping = { "scene": file_name, "layer": layer } output_path = template.format(**mapping) if start_frame: filename_zero = "{}_{:04d}.vrscene".format( output_path, start_frame) else: filename_zero = "{}.vrscene".format(output_path) result = filename_zero.replace("\\", "/") return result ================================================ FILE: openpype/hosts/maya/plugins/publish/extract_workfile_xgen.py ================================================ import os import shutil import copy from maya import cmds import pyblish.api from openpype.hosts.maya.api.alembic import extract_alembic from openpype.pipeline import publish from openpype.lib import StringTemplate class ExtractWorkfileXgen(publish.Extractor): """Extract Workfile Xgen. When submitting a render, we need to prep Xgen side car files. """ # Offset to run before workfile scene save. order = pyblish.api.ExtractorOrder - 0.499 label = "Extract Workfile Xgen" families = ["workfile"] hosts = ["maya"] def get_render_max_frame_range(self, context): """Return start to end frame range including all renderlayers in context. This will return the full frame range which includes all frames of the renderlayer instances to be published/submitted. Args: context (pyblish.api.Context): Current publishing context. Returns: tuple or None: Start frame, end frame tuple if any renderlayers found. Otherwise None is returned. """ def _is_active_renderlayer(i): """Return whether instance is active renderlayer""" if not i.data.get("publish", True): return False is_renderlayer = ( "renderlayer" in i.data.get("families", []) or i.data["family"] == "renderlayer" ) return is_renderlayer start_frame = None end_frame = None for instance in context: if not _is_active_renderlayer(instance): # Only consider renderlyare instances continue render_start_frame = instance.data["frameStart"] render_end_frame = instance.data["frameEnd"] if start_frame is None: start_frame = render_start_frame else: start_frame = min(start_frame, render_start_frame) if end_frame is None: end_frame = render_end_frame else: end_frame = max(end_frame, render_end_frame) if start_frame is None or end_frame is None: return return start_frame, end_frame def process(self, instance): transfers = [] # Validate there is any palettes in the scene. if not cmds.ls(type="xgmPalette"): self.log.debug( "No collections found in the scene. Skipping Xgen extraction." ) return import xgenm # Validate to extract only when we are publishing a renderlayer as # well. render_range = self.get_render_max_frame_range(instance.context) if not render_range: self.log.debug( "No publishable renderlayers found in context. Skipping Xgen" " extraction." ) return start_frame, end_frame = render_range # We decrement start frame and increment end frame so motion blur will # render correctly. start_frame -= 1 end_frame += 1 # Extract patches alembic. path_no_ext, _ = os.path.splitext(instance.context.data["currentFile"]) kwargs = {"attrPrefix": ["xgen"], "stripNamespaces": True} alembic_files = [] for palette in cmds.ls(type="xgmPalette"): patch_names = [] for description in xgenm.descriptions(palette): for name in xgenm.boundGeometry(palette, description): patch_names.append(name) alembic_file = "{}__{}.abc".format( path_no_ext, palette.replace(":", "__ns__") ) extract_alembic( alembic_file, root=patch_names, selection=False, startFrame=float(start_frame), endFrame=float(end_frame), verbose=True, **kwargs ) alembic_files.append(alembic_file) template_data = copy.deepcopy(instance.data["anatomyData"]) published_maya_path = StringTemplate( instance.context.data["anatomy"].templates["publish"]["file"] ).format(template_data) published_basename, _ = os.path.splitext(published_maya_path) for source in alembic_files: destination = os.path.join( os.path.dirname(instance.data["resourcesDir"]), os.path.basename( source.replace(path_no_ext, published_basename) ) ) transfers.append((source, destination)) # Validate that we are using the published workfile. deadline_settings = instance.context.get("deadline") if deadline_settings: publish_settings = deadline_settings["publish"] if not publish_settings["MayaSubmitDeadline"]["use_published"]: self.log.debug( "Not using the published workfile. Abort Xgen extraction." ) return # Collect Xgen and Delta files. xgen_files = [] sources = [] current_dir = os.path.dirname(instance.context.data["currentFile"]) attrs = ["xgFileName", "xgBaseFile"] for palette in cmds.ls(type="xgmPalette"): for attr in attrs: source = os.path.join( current_dir, cmds.getAttr(palette + "." + attr) ) if not os.path.exists(source): continue ext = os.path.splitext(source)[1] if ext == ".xgen": xgen_files.append(source) if ext == ".xgd": sources.append(source) # Copy .xgen file to temporary location and modify. staging_dir = self.staging_dir(instance) for source in xgen_files: destination = os.path.join(staging_dir, os.path.basename(source)) shutil.copy(source, destination) lines = [] with open(destination, "r") as f: for line in [line.rstrip() for line in f]: if line.startswith("\txgProjectPath"): path = os.path.dirname(instance.data["resourcesDir"]) line = "\txgProjectPath\t\t{}/".format( path.replace("\\", "/") ) lines.append(line) with open(destination, "w") as f: f.write("\n".join(lines)) sources.append(destination) # Add resource files to workfile instance. for source in sources: basename = os.path.basename(source) destination = os.path.join( os.path.dirname(instance.data["resourcesDir"]), basename ) transfers.append((source, destination)) destination_dir = os.path.join( instance.data["resourcesDir"], "collections" ) for palette in cmds.ls(type="xgmPalette"): project_path = xgenm.getAttr("xgProjectPath", palette) data_path = xgenm.getAttr("xgDataPath", palette) data_path = data_path.replace("${PROJECT}", project_path) for path in data_path.split(";"): for root, _, files in os.walk(path): for f in files: source = os.path.join(root, f) destination = "{}/{}{}".format( destination_dir, palette.replace(":", "__ns__"), source.replace(path, "") ) transfers.append((source, destination)) for source, destination in transfers: self.log.debug("Transfer: {} > {}".format(source, destination)) instance.data["transfers"] = transfers # Set palette attributes in preparation for workfile publish. attrs = {"xgFileName": None, "xgBaseFile": ""} data = {} for palette in cmds.ls(type="xgmPalette"): attrs["xgFileName"] = "resources/{}.xgen".format( palette.replace(":", "__ns__") ) for attr, value in attrs.items(): node_attr = palette + "." + attr old_value = cmds.getAttr(node_attr) try: data[palette][attr] = old_value except KeyError: data[palette] = {attr: old_value} cmds.setAttr(node_attr, value, type="string") self.log.debug( "Setting \"{}\" on \"{}\"".format(value, node_attr) ) cmds.setAttr(palette + "." + "xgExportAsDelta", False) instance.data["xgenAttributes"] = data ================================================ FILE: openpype/hosts/maya/plugins/publish/extract_xgen.py ================================================ import os import copy import tempfile from maya import cmds import xgenm from openpype.pipeline import publish from openpype.hosts.maya.api.lib import ( maintained_selection, attribute_values, write_xgen_file, delete_after ) from openpype.lib import StringTemplate class ExtractXgen(publish.Extractor): """Extract Xgen Workflow: - Duplicate nodes used for patches. - Export palette and import onto duplicate nodes. - Export/Publish duplicate nodes and palette. - Export duplicate palette to .xgen file and add to publish. - Publish all xgen files as resources. """ label = "Extract Xgen" hosts = ["maya"] families = ["xgen"] scene_type = "ma" def process(self, instance): if "representations" not in instance.data: instance.data["representations"] = [] staging_dir = self.staging_dir(instance) maya_filename = "{}.{}".format(instance.data["name"], self.scene_type) maya_filepath = os.path.join(staging_dir, maya_filename) # Get published xgen file name. template_data = copy.deepcopy(instance.data["anatomyData"]) template_data.update({"ext": "xgen"}) templates = instance.context.data["anatomy"].templates["publish"] xgen_filename = StringTemplate(templates["file"]).format(template_data) xgen_path = os.path.join( self.staging_dir(instance), xgen_filename ).replace("\\", "/") type = "mayaAscii" if self.scene_type == "ma" else "mayaBinary" # Duplicate xgen setup. with delete_after() as delete_bin: duplicate_nodes = [] # Collect nodes to export. for node in instance.data["xgenConnections"]: # Duplicate_transform subd patch geometry. duplicate_transform = cmds.duplicate(node)[0] delete_bin.append(duplicate_transform) # Discard the children. shapes = cmds.listRelatives(duplicate_transform, shapes=True) children = cmds.listRelatives( duplicate_transform, children=True ) cmds.delete(set(children) - set(shapes)) if cmds.listRelatives(duplicate_transform, parent=True): duplicate_transform = cmds.parent( duplicate_transform, world=True )[0] duplicate_nodes.append(duplicate_transform) # Export temp xgen palette files. temp_xgen_path = os.path.join( tempfile.gettempdir(), "temp.xgen" ).replace("\\", "/") xgenm.exportPalette( instance.data["xgmPalette"].replace("|", ""), temp_xgen_path ) self.log.debug("Extracted to {}".format(temp_xgen_path)) # Import xgen onto the duplicate. with maintained_selection(): cmds.select(duplicate_nodes) palette = xgenm.importPalette(temp_xgen_path, []) delete_bin.append(palette) # Copy shading assignments. nodes = ( instance.data["xgmDescriptions"] + instance.data["xgmSubdPatches"] ) for node in nodes: target_node = node.split(":")[-1] shading_engine = cmds.listConnections( node, type="shadingEngine" )[0] cmds.sets(target_node, edit=True, forceElement=shading_engine) # Export duplicated palettes. xgenm.exportPalette(palette, xgen_path) # Export Maya file. attribute_data = {"{}.xgFileName".format(palette): xgen_filename} with attribute_values(attribute_data): with maintained_selection(): cmds.select(duplicate_nodes + [palette]) cmds.file( maya_filepath, force=True, type=type, exportSelected=True, preserveReferences=False, constructionHistory=True, shader=True, constraints=True, expressions=True ) self.log.debug("Extracted to {}".format(maya_filepath)) if os.path.exists(temp_xgen_path): os.remove(temp_xgen_path) data = { "xgDataPath": os.path.join( instance.data["resourcesDir"], "collections", palette.replace(":", "__ns__") ).replace("\\", "/"), "xgProjectPath": os.path.dirname( instance.data["resourcesDir"] ).replace("\\", "/") } write_xgen_file(data, xgen_path) # Adding representations. representation = { "name": "xgen", "ext": "xgen", "files": xgen_filename, "stagingDir": staging_dir, } instance.data["representations"].append(representation) representation = { "name": self.scene_type, "ext": self.scene_type, "files": maya_filename, "stagingDir": staging_dir } instance.data["representations"].append(representation) ================================================ FILE: openpype/hosts/maya/plugins/publish/extract_yeti_cache.py ================================================ import os import json from maya import cmds from openpype.pipeline import publish class ExtractYetiCache(publish.Extractor): """Producing Yeti cache files using scene time range. This will extract Yeti cache file sequence and fur settings. """ label = "Extract Yeti Cache" hosts = ["maya"] families = ["yetiRig", "yeticache"] def process(self, instance): yeti_nodes = cmds.ls(instance, type="pgYetiMaya") if not yeti_nodes: raise RuntimeError("No pgYetiMaya nodes found in the instance") # Define extract output file path dirname = self.staging_dir(instance) # Collect information for writing cache start_frame = instance.data["frameStartHandle"] end_frame = instance.data["frameEndHandle"] preroll = instance.data["preroll"] if preroll > 0: start_frame -= preroll kwargs = {} samples = instance.data.get("samples", 0) if samples == 0: kwargs.update({"sampleTimes": "0.0 1.0"}) else: kwargs.update({"samples": samples}) self.log.debug( "Writing out cache {} - {}".format(start_frame, end_frame)) # Start writing the files for snap shot # will be replace by the Yeti node name path = os.path.join(dirname, " .%04d.fur") cmds.pgYetiCommand(yeti_nodes, writeCache=path, range=(start_frame, end_frame), updateViewport=False, generatePreview=False, **kwargs) cache_files = [x for x in os.listdir(dirname) if x.endswith(".fur")] self.log.debug("Writing metadata file") settings = instance.data["fursettings"] fursettings_path = os.path.join(dirname, "yeti.fursettings") with open(fursettings_path, "w") as fp: json.dump(settings, fp, ensure_ascii=False) # build representations if "representations" not in instance.data: instance.data["representations"] = [] self.log.debug("cache files: {}".format(cache_files[0])) # Workaround: We do not explicitly register these files with the # representation solely so that we can write multiple sequences # a single Subset without renaming - it's a bit of a hack # TODO: Implement better way to manage this sort of integration if 'transfers' not in instance.data: instance.data['transfers'] = [] publish_dir = instance.data["publishDir"] for cache_filename in cache_files: src = os.path.join(dirname, cache_filename) dst = os.path.join(publish_dir, os.path.basename(cache_filename)) instance.data['transfers'].append([src, dst]) instance.data["representations"].append( { 'name': 'fur', 'ext': 'fursettings', 'files': os.path.basename(fursettings_path), 'stagingDir': dirname } ) self.log.debug("Extracted {} to {}".format(instance, dirname)) ================================================ FILE: openpype/hosts/maya/plugins/publish/extract_yeti_rig.py ================================================ # -*- coding: utf-8 -*- """Extract Yeti rig.""" import os import json import contextlib from maya import cmds from openpype.pipeline import publish from openpype.hosts.maya.api import lib @contextlib.contextmanager def disconnect_plugs(settings, members): """Disconnect and store attribute connections.""" members = cmds.ls(members, long=True) original_connections = [] try: for input in settings["inputs"]: # Get source shapes source_nodes = lib.lsattr("cbId", input["sourceID"]) if not source_nodes: continue source = next(s for s in source_nodes if s not in members) # Get destination shapes (the shapes used as hook up) destination_nodes = lib.lsattr("cbId", input["destinationID"]) destination = next(i for i in destination_nodes if i in members) # Create full connection connections = input["connections"] src_attribute = "%s.%s" % (source, connections[0]) dst_attribute = "%s.%s" % (destination, connections[1]) # Check if there is an actual connection if not cmds.isConnected(src_attribute, dst_attribute): print("No connection between %s and %s" % ( src_attribute, dst_attribute)) continue # Break and store connection cmds.disconnectAttr(src_attribute, dst_attribute) original_connections.append([src_attribute, dst_attribute]) yield finally: # Restore previous connections for connection in original_connections: try: cmds.connectAttr(connection[0], connection[1]) except Exception as e: print(e) continue @contextlib.contextmanager def yetigraph_attribute_values(assumed_destination, resources): """Get values from Yeti attributes in graph.""" try: for resource in resources: if "graphnode" not in resource: continue fname = os.path.basename(resource["source"]) new_fpath = os.path.join(assumed_destination, fname) new_fpath = new_fpath.replace("\\", "/") try: cmds.pgYetiGraph(resource["node"], node=resource["graphnode"], param=resource["param"], setParamValueString=new_fpath) except Exception as exc: print(">>> Exception:", exc) yield finally: for resource in resources: if "graphnode" not in resources: continue try: cmds.pgYetiGraph(resource["node"], node=resource["graphnode"], param=resource["param"], setParamValue=resource["source"]) except RuntimeError: pass class ExtractYetiRig(publish.Extractor): """Extract the Yeti rig to a Maya Scene and write the Yeti rig data.""" label = "Extract Yeti Rig" hosts = ["maya"] families = ["yetiRig"] scene_type = "ma" def process(self, instance): """Plugin entry point.""" ext_mapping = ( instance.context.data["project_settings"]["maya"]["ext_mapping"] ) if ext_mapping: self.log.debug("Looking in settings for scene type ...") # use extension mapping for first family found for family in self.families: try: self.scene_type = ext_mapping[family] self.log.debug( "Using {} as scene type".format(self.scene_type)) break except KeyError: # no preset found pass yeti_nodes = cmds.ls(instance, type="pgYetiMaya") if not yeti_nodes: raise RuntimeError("No pgYetiMaya nodes found in the instance") # Define extract output file path dirname = self.staging_dir(instance) settings_path = os.path.join(dirname, "yeti.rigsettings") # Yeti related staging dirs maya_path = os.path.join(dirname, "yeti_rig.{}".format(self.scene_type)) self.log.debug("Writing metadata file: {}".format(settings_path)) image_search_path = resources_dir = instance.data["resourcesDir"] settings = instance.data.get("rigsettings", None) assert settings, "Yeti rig settings were not collected." settings["imageSearchPath"] = image_search_path with open(settings_path, "w") as fp: json.dump(settings, fp, ensure_ascii=False) # add textures to transfers if 'transfers' not in instance.data: instance.data['transfers'] = [] for resource in instance.data.get('resources', []): for file in resource['files']: src = file dst = os.path.join(image_search_path, os.path.basename(file)) instance.data['transfers'].append([src, dst]) self.log.debug("adding transfer {} -> {}". format(src, dst)) # Ensure the imageSearchPath is being remapped to the publish folder attr_value = {"%s.imageSearchPath" % n: str(image_search_path) for n in yeti_nodes} # Get input_SET members input_set = next(i for i in instance if i == "input_SET") # Get all items set_members = cmds.sets(input_set, query=True) or [] set_members += cmds.listRelatives(set_members, allDescendents=True, fullPath=True) or [] members = cmds.ls(set_members, long=True) nodes = instance.data["setMembers"] resources = instance.data.get("resources", {}) with disconnect_plugs(settings, members): with yetigraph_attribute_values(resources_dir, resources): with lib.attribute_values(attr_value): cmds.select(nodes, noExpand=True) cmds.file(maya_path, force=True, exportSelected=True, typ="mayaAscii" if self.scene_type == "ma" else "mayaBinary", # noqa: E501 preserveReferences=False, constructionHistory=True, shader=False) # Ensure files can be stored # build representations if "representations" not in instance.data: instance.data["representations"] = [] self.log.debug("rig file: {}".format(maya_path)) instance.data["representations"].append( { 'name': self.scene_type, 'ext': self.scene_type, 'files': os.path.basename(maya_path), 'stagingDir': dirname } ) self.log.debug("settings file: {}".format(settings_path)) instance.data["representations"].append( { 'name': 'rigsettings', 'ext': 'rigsettings', 'files': os.path.basename(settings_path), 'stagingDir': dirname } ) self.log.debug("Extracted {} to {}".format(instance, dirname)) cmds.select(clear=True) ================================================ FILE: openpype/hosts/maya/plugins/publish/help/submit_maya_remote_publish_deadline.xml ================================================ ================================================ FILE: openpype/hosts/maya/plugins/publish/help/validate_maya_units.xml ================================================ Errors found ## Publish process has errors At least one plugin failed before this plugin, job won't be sent to Deadline for processing before all issues are fixed. ### How to repair? Check all failing plugins (should be highlighted in red) and fix issues if possible. ================================================ FILE: openpype/hosts/maya/plugins/publish/help/validate_node_ids.xml ================================================ Maya scene units ## Invalid maya scene units Detected invalid maya scene units: {issues} ### How to repair? You can automatically repair the scene units by clicking the Repair action on the right. After that restart publishing with Reload button. ================================================ FILE: openpype/hosts/maya/plugins/publish/help/validate_skeletalmesh_hierarchy.xml ================================================ Missing node ids ## Nodes found with missing `cbId` Nodes were detected in your scene which are missing required `cbId` attributes for identification further in the pipeline. ### How to repair? The node ids are auto-generated on scene save, and thus the easiest fix is to save your scene again. After that restart publishing with Reload button. ### Invalid nodes {nodes} ### How could this happen? This often happens if you've generated new nodes but haven't saved your scene after creating the new nodes. ================================================ FILE: openpype/hosts/maya/plugins/publish/increment_current_file_deadline.py ================================================ import pyblish.api class IncrementCurrentFileDeadline(pyblish.api.ContextPlugin): """Increment the current file. Saves the current maya scene with an increased version number. """ label = "Increment current file" order = pyblish.api.IntegratorOrder + 9.0 hosts = ["maya"] families = ["workfile"] optional = True def process(self, context): from maya import cmds from openpype.lib import version_up from openpype.pipeline.publish import get_errored_plugins_from_context errored_plugins = get_errored_plugins_from_context(context) if any(plugin.__name__ == "MayaSubmitDeadline" for plugin in errored_plugins): raise RuntimeError("Skipping incrementing current file because " "submission to deadline failed.") current_filepath = context.data["currentFile"] new_filepath = version_up(current_filepath) # # Ensure the suffix is .ma because we're saving to `mayaAscii` type if new_filepath.endswith(".ma"): fileType = "mayaAscii" elif new_filepath.endswith(".mb"): fileType = "mayaBinary" cmds.file(rename=new_filepath) cmds.file(save=True, force=True, type=fileType) ================================================ FILE: openpype/hosts/maya/plugins/publish/reset_xgen_attributes.py ================================================ from maya import cmds import pyblish.api class ResetXgenAttributes(pyblish.api.InstancePlugin): """Reset Xgen attributes. When the incremental save of the workfile triggers, the Xgen attributes changes so this plugin will change it back to the values before publishing. """ label = "Reset Xgen Attributes." # Offset to run after workfile increment plugin. order = pyblish.api.IntegratorOrder + 10.0 families = ["workfile"] def process(self, instance): xgen_attributes = instance.data.get("xgenAttributes", {}) if not xgen_attributes: return for palette, data in xgen_attributes.items(): for attr, value in data.items(): node_attr = "{}.{}".format(palette, attr) self.log.debug( "Setting \"{}\" on \"{}\"".format(value, node_attr) ) cmds.setAttr(node_attr, value, type="string") cmds.setAttr(palette + ".xgExportAsDelta", True) # Need to save the scene, cause the attribute changes above does not # mark the scene as modified so user can exit without committing the # changes. self.log.debug("Saving changes.") cmds.file(save=True) ================================================ FILE: openpype/hosts/maya/plugins/publish/save_scene.py ================================================ import pyblish.api from openpype.pipeline.workfile.lock_workfile import ( is_workfile_lock_enabled, remove_workfile_lock ) class SaveCurrentScene(pyblish.api.ContextPlugin): """Save current scene """ label = "Save current file" order = pyblish.api.ExtractorOrder - 0.49 hosts = ["maya"] families = ["renderlayer", "workfile"] def process(self, context): import maya.cmds as cmds current = cmds.file(query=True, sceneName=True) assert context.data['currentFile'] == current # If file has no modifications, skip forcing a file save if not cmds.file(query=True, modified=True): self.log.debug("Skipping file save as there " "are no modifications..") return project_name = context.data["projectName"] project_settings = context.data["project_settings"] # remove lockfile before saving if is_workfile_lock_enabled("maya", project_name, project_settings): remove_workfile_lock(current) self.log.info("Saving current file: {}".format(current)) cmds.file(save=True, force=True) ================================================ FILE: openpype/hosts/maya/plugins/publish/validate_alembic_options_defaults.py ================================================ import pyblish.api from openpype.pipeline import OptionalPyblishPluginMixin from openpype.pipeline.publish import RepairAction, PublishValidationError class ValidateAlembicOptionsDefaults( pyblish.api.InstancePlugin, OptionalPyblishPluginMixin ): """Validate the attributes on the instance are defaults.""" order = pyblish.api.ValidatorOrder families = ["pointcache", "animation"] hosts = ["maya"] label = "Validate Alembic Options Defaults" actions = [RepairAction] optional = True @classmethod def _get_plugin_name(self, publish_attributes): for key in ["ExtractAnimation", "ExtractAlembic"]: if key in publish_attributes.keys(): return key @classmethod def _get_settings(self, context): maya_settings = context.data["project_settings"]["maya"] settings = maya_settings["publish"]["ExtractAlembic"] # Flags are a special case since they are a combination of overrides # and default flags from the settings. settings["flags"] = [ x for x in settings["flags"] if x in settings["overrides"] ] return settings @classmethod def _get_publish_attributes(self, instance): attributes = instance.data["publish_attributes"][ self._get_plugin_name( instance.data["publish_attributes"] ) ] settings = self._get_settings(instance.context) # Flags are a special case since they are a combination of exposed # flags and default flags from the settings. So we need to add the # default flags from the settings and ensure unique items. non_exposed_flags = [ x for x in settings["flags"] if x not in settings["overrides"] ] attributes["flags"] = attributes["flags"] + non_exposed_flags return attributes def process(self, instance): if not self.is_active(instance.data): return settings = self._get_settings(instance.context) attributes = self._get_publish_attributes(instance) msg = ( "Alembic Extract setting \"{}\" is not the default value:" "\nCurrent: {}" "\nDefault Value: {}\n" ) errors = [] for key, value in attributes.items(): default_value = settings[key] # Lists are best to compared sorted since we cant rely on the order # of the items. if isinstance(value, list): value = sorted(value) default_value = sorted(default_value) if value != default_value: errors.append(msg.format(key, value, default_value)) if errors: raise PublishValidationError("\n".join(errors)) @classmethod def repair(cls, instance): # Find create instance twin. create_context = instance.context.data["create_context"] create_instance = None for Instance in create_context.instances: if Instance.data["instance_id"] == instance.data["instance_id"]: create_instance = Instance break assert create_instance is not None # Set the settings values on the create context then save to workfile. publish_attributes = instance.data["publish_attributes"] plugin_name = cls._get_plugin_name(publish_attributes) attributes = cls._get_publish_attributes(instance) settings = cls._get_settings(instance.context) create_publish_attributes = create_instance.data["publish_attributes"] for key in attributes.keys(): create_publish_attributes[plugin_name][key] = settings[key] create_context.save_changes() ================================================ FILE: openpype/hosts/maya/plugins/publish/validate_animated_reference.py ================================================ import pyblish.api import openpype.hosts.maya.api.action from openpype.pipeline.publish import ( PublishValidationError, ValidateContentsOrder, OptionalPyblishPluginMixin ) from maya import cmds class ValidateAnimatedReferenceRig(pyblish.api.InstancePlugin, OptionalPyblishPluginMixin): """Validate all nodes in skeletonAnim_SET are referenced""" order = ValidateContentsOrder hosts = ["maya"] families = ["animation.fbx"] label = "Animated Reference Rig" accepted_controllers = ["transform", "locator"] actions = [openpype.hosts.maya.api.action.SelectInvalidAction] optional = False def process(self, instance): if not self.is_active(instance.data): return animated_sets = instance.data.get("animated_skeleton", []) if not animated_sets: self.log.debug( "No nodes found in skeletonAnim_SET. " "Skipping validation of animated reference rig..." ) return for animated_reference in animated_sets: is_referenced = cmds.referenceQuery( animated_reference, isNodeReferenced=True) if not bool(is_referenced): raise PublishValidationError( "All the content in skeletonAnim_SET" " should be referenced nodes" ) invalid_controls = self.validate_controls(animated_sets) if invalid_controls: raise PublishValidationError( "All the content in skeletonAnim_SET" " should be transforms" ) @classmethod def validate_controls(self, set_members): """Check if the controller set contains only accepted node types. Checks if all its set members are within the hierarchy of the root Checks if the node types of the set members valid Args: set_members: list of nodes of the skeleton_anim_set hierarchy: list of nodes which reside under the root node Returns: errors (list) """ # Validate control types invalid = [] set_members = cmds.ls(set_members, long=True) for node in set_members: if cmds.nodeType(node) not in self.accepted_controllers: invalid.append(node) return invalid ================================================ FILE: openpype/hosts/maya/plugins/publish/validate_animation_content.py ================================================ import pyblish.api import openpype.hosts.maya.api.action from openpype.pipeline.publish import ( PublishValidationError, ValidateContentsOrder, OptionalPyblishPluginMixin ) class ValidateAnimationContent(pyblish.api.InstancePlugin, OptionalPyblishPluginMixin): """Adheres to the content of 'animation' product type - Must have collected `out_hierarchy` data. - All nodes in `out_hierarchy` must be in the instance. """ order = ValidateContentsOrder hosts = ["maya"] families = ["animation"] label = "Animation Content" actions = [openpype.hosts.maya.api.action.SelectInvalidAction] optional = False @classmethod def get_invalid(cls, instance): out_set = next((i for i in instance.data["setMembers"] if i.endswith("out_SET")), None) assert out_set, ("Instance '%s' has no objectSet named: `OUT_set`. " "If this instance is an unloaded reference, " "please deactivate by toggling the 'Active' attribute" % instance.name) assert 'out_hierarchy' in instance.data, "Missing `out_hierarchy` data" out_sets = [node for node in instance if node.endswith("out_SET")] msg = "Couldn't find exactly one out_SET: {0}".format(out_sets) assert len(out_sets) == 1, msg # All nodes in the `out_hierarchy` must be among the nodes that are # in the instance. The nodes in the instance are found from the top # group, as such this tests whether all nodes are under that top group. lookup = set(instance[:]) invalid = [node for node in instance.data['out_hierarchy'] if node not in lookup] return invalid def process(self, instance): if not self.is_active(instance.data): return invalid = self.get_invalid(instance) if invalid: raise PublishValidationError( "Animation content is invalid. See log.") ================================================ FILE: openpype/hosts/maya/plugins/publish/validate_animation_out_set_related_node_ids.py ================================================ import maya.cmds as cmds import pyblish.api import openpype.hosts.maya.api.action from openpype.hosts.maya.api import lib from openpype.pipeline.publish import ( RepairAction, ValidateContentsOrder, PublishValidationError, OptionalPyblishPluginMixin ) class ValidateOutRelatedNodeIds(pyblish.api.InstancePlugin, OptionalPyblishPluginMixin): """Validate if deformed shapes have related IDs to the original shapes When a deformer is applied in the scene on a referenced mesh that already had deformers then Maya will create a new shape node for the mesh that does not have the original id. This validator checks whether the ids are valid on all the shape nodes in the instance. """ order = ValidateContentsOrder families = ['animation', "pointcache", "proxyAbc"] hosts = ['maya'] label = 'Animation Out Set Related Node Ids' actions = [ openpype.hosts.maya.api.action.SelectInvalidAction, RepairAction ] optional = False def process(self, instance): """Process all meshes""" if not self.is_active(instance.data): return # Ensure all nodes have a cbId and a related ID to the original shapes # if a deformer has been created on the shape invalid = self.get_invalid(instance) if invalid: # TODO: Message formatting can be improved raise PublishValidationError("Nodes found with mismatching " "IDs: {0}".format(invalid), title="Invalid node ids") @classmethod def get_invalid(cls, instance): """Get all nodes which do not match the criteria""" invalid = [] types_to_skip = ["locator"] # get asset id nodes = instance.data.get("out_hierarchy", instance[:]) for node in nodes: # We only check when the node is *not* referenced if cmds.referenceQuery(node, isNodeReferenced=True): continue # Check if node is a shape as deformers only work on shapes obj_type = cmds.objectType(node, isAType="shape") if not obj_type: continue # Skip specific types if cmds.objectType(node) in types_to_skip: continue # Get the current id of the node node_id = lib.get_id(node) if not node_id: invalid.append(node) continue history_id = lib.get_id_from_sibling(node) if history_id is not None and node_id != history_id: invalid.append(node) return invalid @classmethod def repair(cls, instance): for node in cls.get_invalid(instance): # Get the original id from history history_id = lib.get_id_from_sibling(node) if not history_id: cls.log.error("Could not find ID in history for '%s'", node) continue lib.set_id(node, history_id, overwrite=True) ================================================ FILE: openpype/hosts/maya/plugins/publish/validate_arnold_scene_source.py ================================================ from maya import cmds import pyblish.api from openpype.pipeline.publish import ( ValidateContentsOrder, PublishValidationError ) from openpype.hosts.maya.api.lib import is_visible class ValidateArnoldSceneSource(pyblish.api.InstancePlugin): """Validate Arnold Scene Source. Ensure no nodes are hidden. """ order = ValidateContentsOrder hosts = ["maya"] families = ["ass", "assProxy"] label = "Validate Arnold Scene Source" def process(self, instance): # Validate against having nodes hidden, which will result in the # extraction to ignore the node. nodes = instance.data["members"] + instance.data.get("proxy", []) nodes = [x for x in nodes if cmds.objectType(x, isAType='dagNode')] hidden_nodes = [ x for x in nodes if not is_visible(x, intermediateObject=False) ] if hidden_nodes: raise PublishValidationError( "Found hidden nodes:\n\n{}\n\nPlease unhide for" " publishing.".format("\n".join(hidden_nodes)) ) class ValidateArnoldSceneSourceProxy(pyblish.api.InstancePlugin): """Validate Arnold Scene Source Proxy. When using proxies we need the nodes to share the same names and not be parent to the world. This ends up needing at least two groups with content nodes and proxy nodes in another. """ order = ValidateContentsOrder hosts = ["maya"] families = ["assProxy"] label = "Validate Arnold Scene Source Proxy" def _get_nodes_by_name(self, nodes): ungrouped_nodes = [] nodes_by_name = {} parents = [] for node in nodes: node_split = node.split("|") if len(node_split) == 2: ungrouped_nodes.append(node) parent = "|".join(node_split[:-1]) if parent: parents.append(parent) node_name = node.rsplit("|", 1)[-1].rsplit(":", 1)[-1] nodes_by_name[node_name] = node return ungrouped_nodes, nodes_by_name, parents def process(self, instance): # Validate against nodes directly parented to world. ungrouped_nodes = [] nodes, content_nodes_by_name, content_parents = ( self._get_nodes_by_name(instance.data["members"]) ) ungrouped_nodes.extend(nodes) nodes, proxy_nodes_by_name, proxy_parents = self._get_nodes_by_name( instance.data.get("proxy", []) ) ungrouped_nodes.extend(nodes) if ungrouped_nodes: raise PublishValidationError( "Found nodes parented to the world: {}\n" "All nodes need to be grouped.".format(ungrouped_nodes) ) # Validate for content and proxy nodes amount being the same. if len(instance.data["members"]) != len(instance.data["proxy"]): raise PublishValidationError( "Amount of content nodes ({}) and proxy nodes ({}) needs to " "be the same.\nContent nodes: {}\nProxy nodes:{}".format( len(instance.data["members"]), len(instance.data["proxy"]), instance.data["members"], instance.data["proxy"] ) ) # Validate against content and proxy nodes sharing same parent. if list(set(content_parents) & set(proxy_parents)): raise PublishValidationError( "Content and proxy nodes cannot share the same parent." ) # Validate for content and proxy nodes sharing same names. sorted_content_names = sorted(content_nodes_by_name.keys()) sorted_proxy_names = sorted(proxy_nodes_by_name.keys()) odd_content_names = list( set(sorted_content_names) - set(sorted_proxy_names) ) odd_content_nodes = [ content_nodes_by_name[x] for x in odd_content_names ] odd_proxy_names = list( set(sorted_proxy_names) - set(sorted_content_names) ) odd_proxy_nodes = [ proxy_nodes_by_name[x] for x in odd_proxy_names ] if not sorted_content_names == sorted_proxy_names: raise PublishValidationError( "Content and proxy nodes need to share the same names.\n" "Content nodes not matching: {}\n" "Proxy nodes not matching: {}".format( odd_content_nodes, odd_proxy_nodes ) ) ================================================ FILE: openpype/hosts/maya/plugins/publish/validate_arnold_scene_source_cbid.py ================================================ import pyblish.api from openpype.hosts.maya.api import lib from openpype.pipeline.publish import ( ValidateContentsOrder, PublishValidationError, RepairAction ) class ValidateArnoldSceneSourceCbid(pyblish.api.InstancePlugin): """Validate Arnold Scene Source Cbid. It is required for the proxy and content nodes to share the same cbid. """ order = ValidateContentsOrder hosts = ["maya"] families = ["assProxy"] label = "Validate Arnold Scene Source CBID" actions = [RepairAction] @staticmethod def _get_nodes_by_name(nodes): nodes_by_name = {} for node in nodes: node_name = node.rsplit("|", 1)[-1].rsplit(":", 1)[-1] nodes_by_name[node_name] = node return nodes_by_name @classmethod def get_invalid_couples(cls, instance): nodes_by_name = cls._get_nodes_by_name(instance.data["members"]) proxy_nodes_by_name = cls._get_nodes_by_name(instance.data["proxy"]) invalid_couples = [] for content_name, content_node in nodes_by_name.items(): proxy_node = proxy_nodes_by_name.get(content_name, None) if not proxy_node: cls.log.debug( "Content node '{}' has no matching proxy node.".format( content_node ) ) continue content_id = lib.get_id(content_node) proxy_id = lib.get_id(proxy_node) if content_id != proxy_id: invalid_couples.append((content_node, proxy_node)) return invalid_couples def process(self, instance): # Proxy validation. if not instance.data["proxy"]: return # Validate for proxy nodes sharing the same cbId as content nodes. invalid_couples = self.get_invalid_couples(instance) if invalid_couples: raise PublishValidationError( "Found proxy nodes with mismatching cbid:\n{}".format( invalid_couples ) ) @classmethod def repair(cls, instance): for content_node, proxy_node in cls.get_invalid_couples(instance): lib.set_id(proxy_node, lib.get_id(content_node), overwrite=True) ================================================ FILE: openpype/hosts/maya/plugins/publish/validate_ass_relative_paths.py ================================================ import os import types import maya.cmds as cmds from mtoa.core import createOptions import pyblish.api from openpype.pipeline.publish import ( RepairAction, ValidateContentsOrder, PublishValidationError, OptionalPyblishPluginMixin ) class ValidateAssRelativePaths(pyblish.api.InstancePlugin, OptionalPyblishPluginMixin): """Ensure exporting ass file has set relative texture paths""" order = ValidateContentsOrder hosts = ['maya'] families = ['ass'] label = "ASS has relative texture paths" actions = [RepairAction] optional = False def process(self, instance): if not self.is_active(instance.data): return # we cannot ask this until user open render settings as # `defaultArnoldRenderOptions` doesn't exist errors = [] try: absolute_texture = cmds.getAttr( "defaultArnoldRenderOptions.absolute_texture_paths") absolute_procedural = cmds.getAttr( "defaultArnoldRenderOptions.absolute_procedural_paths") texture_search_path = cmds.getAttr( "defaultArnoldRenderOptions.tspath" ) procedural_search_path = cmds.getAttr( "defaultArnoldRenderOptions.pspath" ) except ValueError: raise PublishValidationError( "Default Arnold options has not been created yet." ) scene_dir, scene_basename = os.path.split(cmds.file(q=True, loc=True)) scene_name, _ = os.path.splitext(scene_basename) if self.maya_is_true(absolute_texture): errors.append("Texture path is set to be absolute") if self.maya_is_true(absolute_procedural): errors.append("Procedural path is set to be absolute") anatomy = instance.context.data["anatomy"] # Use project root variables for multiplatform support, see: # https://docs.arnoldrenderer.com/display/A5AFMUG/Search+Path # ':' as path separator is supported by Arnold for all platforms. keys = anatomy.root_environments().keys() paths = [] for k in keys: paths.append("[{}]".format(k)) self.log.debug("discovered roots: {}".format(":".join(paths))) if ":".join(paths) not in texture_search_path: errors.append(( "Project roots {} are not in texture_search_path: {}" ).format(paths, texture_search_path)) if ":".join(paths) not in procedural_search_path: errors.append(( "Project roots {} are not in procedural_search_path: {}" ).format(paths, procedural_search_path)) if errors: raise PublishValidationError("\n".join(errors)) @classmethod def repair(cls, instance): createOptions() texture_path = cmds.getAttr("defaultArnoldRenderOptions.tspath") procedural_path = cmds.getAttr("defaultArnoldRenderOptions.pspath") # Use project root variables for multiplatform support, see: # https://docs.arnoldrenderer.com/display/A5AFMUG/Search+Path # ':' as path separator is supported by Arnold for all platforms. anatomy = instance.context.data["anatomy"] keys = anatomy.root_environments().keys() paths = [] for k in keys: paths.append("[{}]".format(k)) cmds.setAttr( "defaultArnoldRenderOptions.tspath", ":".join([p for p in paths + [texture_path] if p]), type="string" ) cmds.setAttr( "defaultArnoldRenderOptions.absolute_texture_paths", False ) cmds.setAttr( "defaultArnoldRenderOptions.pspath", ":".join([p for p in paths + [procedural_path] if p]), type="string" ) cmds.setAttr( "defaultArnoldRenderOptions.absolute_procedural_paths", False ) @staticmethod def find_absolute_path(relative_path, all_root_paths): for root_path in all_root_paths: possible_path = os.path.join(root_path, relative_path) if os.path.exists(possible_path): return possible_path def maya_is_true(self, attr_val): """ Whether a Maya attr evaluates to True. When querying an attribute value from an ambiguous object the Maya API will return a list of values, which need to be properly handled to evaluate properly. """ if isinstance(attr_val, bool): return attr_val elif isinstance(attr_val, (list, types.GeneratorType)): return any(attr_val) else: return bool(attr_val) ================================================ FILE: openpype/hosts/maya/plugins/publish/validate_assembly_name.py ================================================ import pyblish.api import maya.cmds as cmds import openpype.hosts.maya.api.action from openpype.pipeline.publish import ( PublishValidationError, OptionalPyblishPluginMixin ) class ValidateAssemblyName(pyblish.api.InstancePlugin, OptionalPyblishPluginMixin): """ Ensure Assembly name ends with `GRP` Check if assembly name ends with `_GRP` string. """ label = "Validate Assembly Name" order = pyblish.api.ValidatorOrder families = ["assembly"] actions = [openpype.hosts.maya.api.action.SelectInvalidAction] active = False optional = True @classmethod def get_invalid(cls, instance): cls.log.debug("Checking name of {}".format(instance.name)) content_instance = instance.data.get("setMembers", None) if not content_instance: cls.log.error("Instance has no nodes!") return True # All children will be included in the extracted export so we also # validate *all* descendents of the set members and we skip any # intermediate shapes descendants = cmds.listRelatives(content_instance, allDescendents=True, fullPath=True) or [] descendants = cmds.ls( descendants, noIntermediate=True, type="transform") content_instance = list(set(content_instance + descendants)) assemblies = cmds.ls(content_instance, assemblies=True, long=True) invalid = [] for cr in assemblies: if not cr.endswith('_GRP'): cls.log.error("{} doesn't end with _GRP".format(cr)) invalid.append(cr) return invalid def process(self, instance): if not self.is_active(instance.data): return invalid = self.get_invalid(instance) if invalid: raise PublishValidationError("Found {} invalid named assembly " "items".format(len(invalid))) ================================================ FILE: openpype/hosts/maya/plugins/publish/validate_assembly_namespaces.py ================================================ import pyblish.api import openpype.hosts.maya.api.action from openpype.pipeline.publish import ( PublishValidationError, OptionalPyblishPluginMixin ) class ValidateAssemblyNamespaces(pyblish.api.InstancePlugin, OptionalPyblishPluginMixin): """Ensure namespaces are not nested In the outliner an item in a normal namespace looks as following: props_desk_01_:modelDefault Any namespace which diverts from that is illegal, example of an illegal namespace: room_study_01_:props_desk_01_:modelDefault """ label = "Validate Assembly Namespaces" order = pyblish.api.ValidatorOrder families = ["assembly"] actions = [openpype.hosts.maya.api.action.SelectInvalidAction] optional = False def process(self, instance): if not self.is_active(instance.data): return self.log.debug("Checking namespace for %s" % instance.name) if self.get_invalid(instance): raise PublishValidationError("Nested namespaces found") @classmethod def get_invalid(cls, instance): from maya import cmds invalid = [] for item in cmds.ls(instance): item_parts = item.split("|", 1)[0].rsplit(":") if len(item_parts[:-1]) > 1: invalid.append(item) return invalid ================================================ FILE: openpype/hosts/maya/plugins/publish/validate_assembly_transforms.py ================================================ import pyblish.api from maya import cmds import openpype.hosts.maya.api.action from openpype.pipeline.publish import ( PublishValidationError, RepairAction, OptionalPyblishPluginMixin ) class ValidateAssemblyModelTransforms(pyblish.api.InstancePlugin, OptionalPyblishPluginMixin): """Verify only root nodes of the loaded asset have transformations. Note: This check is temporary and is subject to change. Example outliner: <> means referenced =================================================================== setdress_GRP| props_GRP| barrel_01_:modelDefault| [can have transforms] <> barrel_01_:barrel_GRP [CAN'T have transforms] fence_01_:modelDefault| [can have transforms] <> fence_01_:fence_GRP [CAN'T have transforms] """ order = pyblish.api.ValidatorOrder + 0.49 label = "Assembly Model Transforms" families = ["assembly"] actions = [openpype.hosts.maya.api.action.SelectInvalidAction, RepairAction] prompt_message = ("You are about to reset the matrix to the default values." " This can alter the look of your scene. " "Are you sure you want to continue?") optional = False def process(self, instance): if not self.is_active(instance.data): return invalid = self.get_invalid(instance) if invalid: raise PublishValidationError( ("Found {} invalid transforms of assembly " "items").format(len(invalid))) @classmethod def get_invalid(cls, instance): from openpype.hosts.maya.api import lib # Get all transforms in the loaded containers container_roots = cmds.listRelatives(instance.data["nodesHierarchy"], children=True, type="transform", fullPath=True) transforms_in_container = cmds.listRelatives(container_roots, allDescendents=True, type="transform", fullPath=True) # Extra check due to the container roots still being passed through transforms_in_container = [i for i in transforms_in_container if i not in container_roots] # Ensure all are identity matrix invalid = [] for transform in transforms_in_container: node_matrix = cmds.xform(transform, query=True, matrix=True, objectSpace=True) if not lib.matrix_equals(node_matrix, lib.DEFAULT_MATRIX): invalid.append(transform) return invalid @classmethod def repair(cls, instance): """Reset matrix for illegally transformed nodes We want to ensure the user knows the reset will alter the look of the current scene because the transformations were done on asset nodes instead of the asset top node. Args: instance: Returns: None """ from qtpy import QtWidgets from openpype.hosts.maya.api import lib # Store namespace in variable, cosmetics thingy choice = QtWidgets.QMessageBox.warning( None, "Matrix reset", cls.prompt_message, QtWidgets.QMessageBox.Ok | QtWidgets.QMessageBox.Cancel ) invalid = cls.get_invalid(instance) if not invalid: cls.log.info("No invalid nodes") return if choice: cmds.xform(invalid, matrix=lib.DEFAULT_MATRIX, objectSpace=True) ================================================ FILE: openpype/hosts/maya/plugins/publish/validate_attributes.py ================================================ from collections import defaultdict import pyblish.api from maya import cmds from openpype.hosts.maya.api.lib import set_attribute from openpype.pipeline.publish import ( OptionalPyblishPluginMixin, PublishValidationError, RepairAction, ValidateContentsOrder) class ValidateAttributes(pyblish.api.InstancePlugin, OptionalPyblishPluginMixin): """Ensure attributes are consistent. Attributes to validate and their values comes from the "maya/attributes.json" preset, which needs this structure: { "family": { "node_name.attribute_name": attribute_value } } """ order = ValidateContentsOrder label = "Attributes" hosts = ["maya"] actions = [RepairAction] optional = True attributes = None def process(self, instance): if not self.is_active(instance.data): return # Check for preset existence. if not self.attributes: return invalid = self.get_invalid(instance, compute=True) if invalid: raise PublishValidationError( "Found attributes with invalid values: {}".format(invalid) ) @classmethod def get_invalid(cls, instance, compute=False): if compute: return cls.get_invalid_attributes(instance) else: return instance.data.get("invalid_attributes", []) @classmethod def get_invalid_attributes(cls, instance): invalid_attributes = [] # Filter families. families = [instance.data["family"]] families += instance.data.get("families", []) families = set(families) & set(cls.attributes.keys()) if not families: return [] # Get all attributes to validate. attributes = defaultdict(dict) for family in families: if family not in cls.attributes: # No attributes to validate for family continue for preset_attr, preset_value in cls.attributes[family].items(): node_name, attribute_name = preset_attr.split(".", 1) attributes[node_name][attribute_name] = preset_value if not attributes: return [] # Get invalid attributes. nodes = cmds.ls(long=True) for node in nodes: node_name = node.rsplit("|", 1)[-1].rsplit(":", 1)[-1] if node_name not in attributes: continue for attr_name, expected in attributes[node_name].items(): # Skip if attribute does not exist if not cmds.attributeQuery(attr_name, node=node, exists=True): continue plug = "{}.{}".format(node, attr_name) value = cmds.getAttr(plug) if value != expected: invalid_attributes.append( { "attribute": plug, "expected": expected, "current": value } ) instance.data["invalid_attributes"] = invalid_attributes return invalid_attributes @classmethod def repair(cls, instance): invalid = cls.get_invalid(instance) for data in invalid: node, attr = data["attribute"].split(".", 1) value = data["expected"] set_attribute(node=node, attribute=attr, value=value) ================================================ FILE: openpype/hosts/maya/plugins/publish/validate_camera_attributes.py ================================================ import pyblish.api from maya import cmds import openpype.hosts.maya.api.action from openpype.pipeline.publish import ( PublishValidationError, ValidateContentsOrder, OptionalPyblishPluginMixin ) class ValidateCameraAttributes(pyblish.api.InstancePlugin, OptionalPyblishPluginMixin): """Validates Camera has no invalid attribute keys or values. The Alembic file format does not a specific subset of attributes as such we validate that no values are set there as the output will not match the current scene. For example the preScale, film offsets and film roll. """ order = ValidateContentsOrder families = ['camera'] hosts = ['maya'] label = 'Camera Attributes' actions = [openpype.hosts.maya.api.action.SelectInvalidAction] optional = True DEFAULTS = [ ("filmFitOffset", 0.0), ("horizontalFilmOffset", 0.0), ("verticalFilmOffset", 0.0), ("preScale", 1.0), ("filmTranslateH", 0.0), ("filmTranslateV", 0.0), ("filmRollValue", 0.0) ] @classmethod def get_invalid(cls, instance): # get cameras members = instance.data['setMembers'] shapes = cmds.ls(members, dag=True, shapes=True, long=True) cameras = cmds.ls(shapes, type='camera', long=True) invalid = set() for cam in cameras: for attr, default_value in cls.DEFAULTS: plug = "{}.{}".format(cam, attr) value = cmds.getAttr(plug) # Check if is default value if value != default_value: cls.log.warning("Invalid attribute value: {0} " "(should be: {1}))".format(plug, default_value)) invalid.add(cam) if cmds.listConnections(plug, source=True, destination=False): # TODO: Validate correctly whether value always correct cls.log.warning("%s has incoming connections, validation " "is unpredictable." % plug) return list(invalid) def process(self, instance): """Process all the nodes in the instance""" if not self.is_active(instance.data): return invalid = self.get_invalid(instance) if invalid: raise PublishValidationError( "Invalid camera attributes: {}".format(invalid)) ================================================ FILE: openpype/hosts/maya/plugins/publish/validate_camera_contents.py ================================================ import pyblish.api from maya import cmds import openpype.hosts.maya.api.action from openpype.pipeline.publish import ( PublishValidationError, ValidateContentsOrder, OptionalPyblishPluginMixin) class ValidateCameraContents(pyblish.api.InstancePlugin, OptionalPyblishPluginMixin): """Validates Camera instance contents. A Camera instance may only hold a SINGLE camera's transform, nothing else. It may hold a "locator" as shape, but different shapes are down the hierarchy. """ order = ValidateContentsOrder families = ['camera'] hosts = ['maya'] label = 'Camera Contents' actions = [openpype.hosts.maya.api.action.SelectInvalidAction] validate_shapes = True optional = False @classmethod def get_invalid(cls, instance): # get cameras members = instance.data['setMembers'] shapes = cmds.ls(members, dag=True, shapes=True, long=True) # single camera invalid = [] cameras = cmds.ls(shapes, type='camera', long=True) if len(cameras) != 1: cls.log.error("Camera instance must have a single camera. " "Found {0}: {1}".format(len(cameras), cameras)) invalid.extend(cameras) # We need to check this edge case because returning an extended # list when there are no actual cameras results in # still an empty 'invalid' list if len(cameras) < 1: if members: # If there are members in the instance return all of # them as 'invalid' so the user can still select invalid cls.log.error("No cameras found in instance " "members: {}".format(members)) return members raise PublishValidationError( "No cameras found in empty instance.") if not cls.validate_shapes: cls.log.debug("Not validating shapes in the camera content" " because 'validate shapes' is disabled") return invalid # non-camera shapes valid_shapes = cmds.ls(shapes, type=('camera', 'locator'), long=True) shapes = set(shapes) - set(valid_shapes) if shapes: shapes = list(shapes) cls.log.error("Camera instance should only contain camera " "shapes. Found: {0}".format(shapes)) invalid.extend(shapes) invalid = list(set(invalid)) return invalid def process(self, instance): """Process all the nodes in the instance""" if not self.is_active(instance.data): return invalid = self.get_invalid(instance) if invalid: raise PublishValidationError("Invalid camera contents: " "{0}".format(invalid)) ================================================ FILE: openpype/hosts/maya/plugins/publish/validate_color_sets.py ================================================ from maya import cmds import pyblish.api import openpype.hosts.maya.api.action from openpype.pipeline.publish import ( ValidateMeshOrder, OptionalPyblishPluginMixin, PublishValidationError, RepairAction ) class ValidateColorSets(pyblish.api.Validator, OptionalPyblishPluginMixin): """Validate all meshes in the instance have unlocked normals These can be removed manually through: Modeling > Mesh Display > Color Sets Editor """ order = ValidateMeshOrder hosts = ['maya'] families = ['model'] label = 'Mesh ColorSets' actions = [ openpype.hosts.maya.api.action.SelectInvalidAction, RepairAction ] optional = True @staticmethod def has_color_sets(mesh): """Return whether a mesh node has locked normals""" return cmds.polyColorSet(mesh, allColorSets=True, query=True) @classmethod def get_invalid(cls, instance): """Return the meshes with ColorSets in instance""" meshes = cmds.ls(instance, type='mesh', long=True) return [mesh for mesh in meshes if cls.has_color_sets(mesh)] def process(self, instance): """Raise invalid when any of the meshes have ColorSets""" if not self.is_active(instance.data): return invalid = self.get_invalid(instance) if invalid: raise PublishValidationError( message="Meshes found with Color Sets: {0}".format(invalid) ) @classmethod def repair(cls, instance): """Remove all Color Sets on the meshes in this instance.""" invalid = cls.get_invalid(instance) for mesh in invalid: for set in cmds.polyColorSet(mesh, acs=True, q=True): cmds.polyColorSet(mesh, colorSet=set, delete=True) ================================================ FILE: openpype/hosts/maya/plugins/publish/validate_current_renderlayer_renderable.py ================================================ import pyblish.api from maya import cmds from openpype.pipeline.publish import ( context_plugin_should_run, OptionalPyblishPluginMixin ) class ValidateCurrentRenderLayerIsRenderable(pyblish.api.ContextPlugin, OptionalPyblishPluginMixin): """Validate if current render layer has a renderable camera There is a bug in Redshift which occurs when the current render layer at file open has no renderable camera. The error raised is as follows: "No renderable cameras found. Aborting render" This error is raised even if that render layer will not be rendered. """ label = "Current Render Layer Has Renderable Camera" order = pyblish.api.ValidatorOrder hosts = ["maya"] families = ["renderlayer"] optional = False def process(self, context): if not self.is_active(context.data): return # Workaround bug pyblish-base#250 if not context_plugin_should_run(self, context): return layer = cmds.editRenderLayerGlobals(query=True, currentRenderLayer=True) cameras = cmds.ls(type="camera", long=True) renderable = any(c for c in cameras if cmds.getAttr(c + ".renderable")) assert renderable, ("Current render layer '%s' has no renderable " "camera" % layer) ================================================ FILE: openpype/hosts/maya/plugins/publish/validate_cycle_error.py ================================================ import pyblish.api from maya import cmds import openpype.hosts.maya.api.action from openpype.hosts.maya.api.lib import maintained_selection from openpype.pipeline.publish import ( OptionalPyblishPluginMixin, PublishValidationError, ValidateContentsOrder) class ValidateCycleError(pyblish.api.InstancePlugin, OptionalPyblishPluginMixin): """Validate nodes produce no cycle errors.""" order = ValidateContentsOrder + 0.05 label = "Cycle Errors" hosts = ["maya"] families = ["rig"] actions = [openpype.hosts.maya.api.action.SelectInvalidAction] optional = True def process(self, instance): if not self.is_active(instance.data): return invalid = self.get_invalid(instance) if invalid: raise PublishValidationError( "Nodes produce a cycle error: {}".format(invalid)) @classmethod def get_invalid(cls, instance): with maintained_selection(): cmds.select(instance[:], noExpand=True) plugs = cmds.cycleCheck(all=False, # check selection only list=True) invalid = cmds.ls(plugs, objectsOnly=True, long=True) return invalid ================================================ FILE: openpype/hosts/maya/plugins/publish/validate_frame_range.py ================================================ import pyblish.api from maya import cmds from openpype.pipeline.publish import ( RepairAction, ValidateContentsOrder, PublishValidationError, OptionalPyblishPluginMixin ) from openpype.hosts.maya.api.lib_rendersetup import ( get_attr_overrides, get_attr_in_layer, ) from maya.app.renderSetup.model.override import AbsOverride class ValidateFrameRange(pyblish.api.InstancePlugin, OptionalPyblishPluginMixin): """Validates the frame ranges. This is an optional validator checking if the frame range on instance matches the frame range specified for the asset. It also validates render frame ranges of render layers. Repair action will change everything to match the asset frame range. This can be turned off by the artist to allow custom ranges. """ label = "Validate Frame Range" order = ValidateContentsOrder families = ["animation", "pointcache", "camera", "proxyAbc", "renderlayer", "review", "yeticache"] optional = True actions = [RepairAction] exclude_families = [] def process(self, instance): if not self.is_active(instance.data): return context = instance.context if instance.data.get("tileRendering"): self.log.debug( "Skipping frame range validation because " "tile rendering is enabled." ) return frame_start_handle = int(context.data.get("frameStartHandle")) frame_end_handle = int(context.data.get("frameEndHandle")) handle_start = int(context.data.get("handleStart")) handle_end = int(context.data.get("handleEnd")) frame_start = int(context.data.get("frameStart")) frame_end = int(context.data.get("frameEnd")) inst_start = int(instance.data.get("frameStartHandle")) inst_end = int(instance.data.get("frameEndHandle")) inst_frame_start = int(instance.data.get("frameStart")) inst_frame_end = int(instance.data.get("frameEnd")) inst_handle_start = int(instance.data.get("handleStart")) inst_handle_end = int(instance.data.get("handleEnd")) # basic sanity checks assert frame_start_handle <= frame_end_handle, ( "start frame is lower then end frame") # compare with data on instance errors = [] if [ef for ef in self.exclude_families if instance.data["family"] in ef]: return if (inst_start != frame_start_handle): errors.append("Instance start frame [ {} ] doesn't " "match the one set on asset [ {} ]: " "{}/{}/{}/{} (handle/start/end/handle)".format( inst_start, frame_start_handle, handle_start, frame_start, frame_end, handle_end )) if (inst_end != frame_end_handle): errors.append("Instance end frame [ {} ] doesn't " "match the one set on asset [ {} ]: " "{}/{}/{}/{} (handle/start/end/handle)".format( inst_end, frame_end_handle, handle_start, frame_start, frame_end, handle_end )) checks = { "frame start": (frame_start, inst_frame_start), "frame end": (frame_end, inst_frame_end), "handle start": (handle_start, inst_handle_start), "handle end": (handle_end, inst_handle_end) } for label, values in checks.items(): if values[0] != values[1]: errors.append( "{} on instance ({}) does not match with the asset " "({}).".format(label.title(), values[1], values[0]) ) if errors: report = "Frame range settings are incorrect.\n\n" for error in errors: report += "- {}\n\n".format(error) raise PublishValidationError(report, title="Frame Range incorrect") @classmethod def repair(cls, instance): """ Repair instance container to match asset data. """ if "renderlayer" in instance.data.get("families"): # Special behavior for renderlayers cls.repair_renderlayer(instance) return node = instance.data["name"] context = instance.context frame_start_handle = int(context.data.get("frameStartHandle")) frame_end_handle = int(context.data.get("frameEndHandle")) handle_start = int(context.data.get("handleStart")) handle_end = int(context.data.get("handleEnd")) frame_start = int(context.data.get("frameStart")) frame_end = int(context.data.get("frameEnd")) # Start if cmds.attributeQuery("handleStart", node=node, exists=True): cmds.setAttr("{}.handleStart".format(node), handle_start) cmds.setAttr("{}.frameStart".format(node), frame_start) else: # Include start handle in frame start if no separate handleStart # attribute exists on the node cmds.setAttr("{}.frameStart".format(node), frame_start_handle) # End if cmds.attributeQuery("handleEnd", node=node, exists=True): cmds.setAttr("{}.handleEnd".format(node), handle_end) cmds.setAttr("{}.frameEnd".format(node), frame_end) else: # Include end handle in frame end if no separate handleEnd # attribute exists on the node cmds.setAttr("{}.frameEnd".format(node), frame_end_handle) @classmethod def repair_renderlayer(cls, instance): """Apply frame range in render settings""" layer = instance.data["renderlayer"] context = instance.context start_attr = "defaultRenderGlobals.startFrame" end_attr = "defaultRenderGlobals.endFrame" frame_start_handle = int(context.data.get("frameStartHandle")) frame_end_handle = int(context.data.get("frameEndHandle")) cls._set_attr_in_layer(start_attr, layer, frame_start_handle) cls._set_attr_in_layer(end_attr, layer, frame_end_handle) @classmethod def _set_attr_in_layer(cls, node_attr, layer, value): if get_attr_in_layer(node_attr, layer=layer) == value: # Already ok. This can happen if you have multiple renderlayers # validated and there are no frame range overrides. The first # layer's repair would have fixed the global value already return overrides = list(get_attr_overrides(node_attr, layer=layer)) if overrides: # We set the last absolute override if it is an absolute override # otherwise we'll add an Absolute override last_override = overrides[-1][1] if not isinstance(last_override, AbsOverride): collection = last_override.parent() node, attr = node_attr.split(".", 1) last_override = collection.createAbsoluteOverride(node, attr) cls.log.debug("Setting {attr} absolute override in " "layer '{layer}': {value}".format(layer=layer, attr=node_attr, value=value)) cmds.setAttr(last_override.name() + ".attrValue", value) else: # Set the attribute directly # (Note that this will set the global attribute) cls.log.debug("Setting global {attr}: {value}".format( attr=node_attr, value=value )) cmds.setAttr(node_attr, value) ================================================ FILE: openpype/hosts/maya/plugins/publish/validate_glsl_material.py ================================================ import os from maya import cmds import pyblish.api from openpype.pipeline.publish import ( RepairAction, ValidateContentsOrder ) from openpype.pipeline import ( PublishValidationError, OptionalPyblishPluginMixin ) class ValidateGLSLMaterial(pyblish.api.InstancePlugin, OptionalPyblishPluginMixin): """ Validate if the asset uses GLSL Shader """ order = ValidateContentsOrder + 0.1 families = ['gltf'] hosts = ['maya'] label = 'GLSL Shader for GLTF' actions = [RepairAction] optional = True active = True def process(self, instance): if not self.is_active(instance.data): return shading_grp = self.get_material_from_shapes(instance) if not shading_grp: raise PublishValidationError("No shading group found") invalid = self.get_texture_shader_invalid(instance) if invalid: raise PublishValidationError("Non GLSL Shader found: " "{0}".format(invalid)) def get_material_from_shapes(self, instance): shapes = cmds.ls(instance, type="mesh", long=True) for shape in shapes: shading_grp = cmds.listConnections(shape, destination=True, type="shadingEngine") return shading_grp or [] def get_texture_shader_invalid(self, instance): invalid = set() shading_grp = self.get_material_from_shapes(instance) for shading_group in shading_grp: material_name = "{}.surfaceShader".format(shading_group) material = cmds.listConnections(material_name, source=True, destination=False, type="GLSLShader") if not material: # add material name material = cmds.listConnections(material_name)[0] invalid.add(material) return list(invalid) @classmethod def repair(cls, instance): """ Repair instance by assigning GLSL Shader to the material """ cls.assign_glsl_shader(instance) return @classmethod def assign_glsl_shader(cls, instance): """ Converting StingrayPBS material to GLSL Shaders for the glb export through Maya2GLTF plugin """ meshes = cmds.ls(instance, type="mesh", long=True) cls.log.debug("meshes: {}".format(meshes)) # load the glsl shader plugin cmds.loadPlugin("glslShader", quiet=True) for mesh in meshes: # create glsl shader glsl = cmds.createNode('GLSLShader') glsl_shading_grp = cmds.sets(name=glsl + "SG", empty=True, renderable=True, noSurfaceShader=True) cmds.connectAttr(glsl + ".outColor", glsl_shading_grp + ".surfaceShader") # load the maya2gltf shader ogsfx_path = instance.context.data["project_settings"]["maya"]["publish"]["ExtractGLB"]["ogsfx_path"] # noqa if not os.path.exists(ogsfx_path): if ogsfx_path: # if custom ogsfx path is not specified # the log below is the warning for the user cls.log.warning("ogsfx shader file " "not found in {}".format(ogsfx_path)) cls.log.debug("Searching the ogsfx shader file in " "default maya directory...") # re-direct to search the ogsfx path in maya_dir ogsfx_path = os.getenv("MAYA_APP_DIR") + ogsfx_path if not os.path.exists(ogsfx_path): raise PublishValidationError("The ogsfx shader file does not " # noqa "exist: {}".format(ogsfx_path)) # noqa cmds.setAttr(glsl + ".shader", ogsfx_path, typ="string") # list the materials used for the assets shading_grp = cmds.listConnections(mesh, destination=True, type="shadingEngine") # get the materials related to the selected assets for material in shading_grp: pbs_shader = cmds.listConnections(material, destination=True, type="StingrayPBS") if pbs_shader: cls.pbs_shader_conversion(pbs_shader, glsl) # setting up to relink the texture if # the mesh is with aiStandardSurface arnold_shader = cmds.listConnections(material, destination=True, type="aiStandardSurface") if arnold_shader: cls.arnold_shader_conversion(arnold_shader, glsl) cmds.sets(mesh, forceElement=str(glsl_shading_grp)) @classmethod def pbs_shader_conversion(cls, main_shader, glsl): cls.log.debug("StringrayPBS detected " "-> Can do texture conversion") for shader in main_shader: # get the file textures related to the PBS Shader albedo = cmds.listConnections(shader + ".TEX_color_map") if albedo: dif_output = albedo[0] + ".outColor" # get the glsl_shader input # reconnect the file nodes to maya2gltf shader glsl_dif = glsl + ".u_BaseColorTexture" cmds.connectAttr(dif_output, glsl_dif) # connect orm map if there is one orm_packed = cmds.listConnections(shader + ".TEX_ao_map") if orm_packed: orm_output = orm_packed[0] + ".outColor" mtl = glsl + ".u_MetallicTexture" ao = glsl + ".u_OcclusionTexture" rough = glsl + ".u_RoughnessTexture" cmds.connectAttr(orm_output, mtl) cmds.connectAttr(orm_output, ao) cmds.connectAttr(orm_output, rough) # connect nrm map if there is one nrm = cmds.listConnections(shader + ".TEX_normal_map") if nrm: nrm_output = nrm[0] + ".outColor" glsl_nrm = glsl + ".u_NormalTexture" cmds.connectAttr(nrm_output, glsl_nrm) @classmethod def arnold_shader_conversion(cls, main_shader, glsl): cls.log.debug("aiStandardSurface detected " "-> Can do texture conversion") for shader in main_shader: # get the file textures related to the PBS Shader albedo = cmds.listConnections(shader + ".baseColor") if albedo: dif_output = albedo[0] + ".outColor" # get the glsl_shader input # reconnect the file nodes to maya2gltf shader glsl_dif = glsl + ".u_BaseColorTexture" cmds.connectAttr(dif_output, glsl_dif) orm_packed = cmds.listConnections(shader + ".specularRoughness") if orm_packed: orm_output = orm_packed[0] + ".outColor" mtl = glsl + ".u_MetallicTexture" ao = glsl + ".u_OcclusionTexture" rough = glsl + ".u_RoughnessTexture" cmds.connectAttr(orm_output, mtl) cmds.connectAttr(orm_output, ao) cmds.connectAttr(orm_output, rough) # connect nrm map if there is one bump_node = cmds.listConnections(shader + ".normalCamera") if bump_node: for bump in bump_node: nrm = cmds.listConnections(bump + ".bumpValue") if nrm: nrm_output = nrm[0] + ".outColor" glsl_nrm = glsl + ".u_NormalTexture" cmds.connectAttr(nrm_output, glsl_nrm) ================================================ FILE: openpype/hosts/maya/plugins/publish/validate_glsl_plugin.py ================================================ from maya import cmds import pyblish.api from openpype.pipeline.publish import ( RepairAction, ValidateContentsOrder, PublishValidationError, OptionalPyblishPluginMixin ) class ValidateGLSLPlugin(pyblish.api.InstancePlugin, OptionalPyblishPluginMixin): """ Validate if the asset uses GLSL Shader """ order = ValidateContentsOrder + 0.15 families = ['gltf'] hosts = ['maya'] label = 'maya2glTF plugin' actions = [RepairAction] optional = False def process(self, instance): if not self.is_active(instance.data): return if not cmds.pluginInfo("maya2glTF", query=True, loaded=True): raise PublishValidationError("maya2glTF is not loaded") @classmethod def repair(cls, instance): """ Repair instance by enabling the plugin """ return cmds.loadPlugin("maya2glTF", quiet=True) ================================================ FILE: openpype/hosts/maya/plugins/publish/validate_instance_has_members.py ================================================ import pyblish.api import openpype.hosts.maya.api.action from openpype.pipeline.publish import ( ValidateContentsOrder, PublishValidationError ) class ValidateInstanceHasMembers(pyblish.api.InstancePlugin): """Validates instance objectSet has *any* members.""" order = ValidateContentsOrder hosts = ["maya"] label = 'Instance has members' actions = [openpype.hosts.maya.api.action.SelectInvalidAction] @classmethod def get_invalid(cls, instance): invalid = list() if not instance.data.get("setMembers"): objectset_name = instance.data['name'] invalid.append(objectset_name) return invalid def process(self, instance): # Allow renderlayer, rendersetup and workfile to be empty skip_families = {"workfile", "renderlayer", "rendersetup"} if instance.data.get("family") in skip_families: return invalid = self.get_invalid(instance) if invalid: # Invalid will always be a single entry, we log the single name name = invalid[0] raise PublishValidationError( title="Empty instance", message="Instance '{0}' is empty".format(name) ) ================================================ FILE: openpype/hosts/maya/plugins/publish/validate_instance_in_context.py ================================================ # -*- coding: utf-8 -*- """Validate if instance asset is the same as context asset.""" from __future__ import absolute_import import pyblish.api from openpype import AYON_SERVER_ENABLED import openpype.hosts.maya.api.action from openpype.pipeline.publish import ( RepairAction, ValidateContentsOrder, PublishValidationError, OptionalPyblishPluginMixin ) from maya import cmds class ValidateInstanceInContext(pyblish.api.InstancePlugin, OptionalPyblishPluginMixin): """Validator to check if instance asset match context asset. When working in per-shot style you always publish data in context of current asset (shot). This validator checks if this is so. It is optional so it can be disabled when needed. Action on this validator will select invalid instances in Outliner. """ order = ValidateContentsOrder label = "Instance in same Context" optional = True hosts = ["maya"] actions = [ openpype.hosts.maya.api.action.SelectInvalidAction, RepairAction ] def process(self, instance): if not self.is_active(instance.data): return asset = instance.data.get("asset") context_asset = self.get_context_asset(instance) if asset != context_asset: raise PublishValidationError( message=( "Instance '{}' publishes to different asset than current " "context: {}. Current context: {}".format( instance.name, asset, context_asset ) ), description=( "## Publishing to a different asset\n" "There are publish instances present which are publishing " "into a different asset than your current context.\n\n" "Usually this is not what you want but there can be cases " "where you might want to publish into another asset or " "shot. If that's the case you can disable the validation " "on the instance to ignore it." ) ) @classmethod def get_invalid(cls, instance): return [instance.data["instance_node"]] @classmethod def repair(cls, instance): context_asset = cls.get_context_asset(instance) instance_node = instance.data["instance_node"] if AYON_SERVER_ENABLED: asset_name_attr = "folderPath" else: asset_name_attr = "asset" cmds.setAttr( "{}.{}".format(instance_node, asset_name_attr), context_asset, type="string" ) @staticmethod def get_context_asset(instance): return instance.context.data["asset"] ================================================ FILE: openpype/hosts/maya/plugins/publish/validate_instance_subset.py ================================================ import pyblish.api import string import six from openpype.pipeline.publish import ( ValidateContentsOrder, PublishValidationError ) # Allow only characters, numbers and underscore allowed = set(string.ascii_lowercase + string.ascii_uppercase + string.digits + '_') def validate_name(subset): return all(x in allowed for x in subset) class ValidateSubsetName(pyblish.api.InstancePlugin): """Validates subset name has only valid characters""" order = ValidateContentsOrder families = ["*"] label = "Subset Name" def process(self, instance): subset = instance.data.get("subset", None) # Ensure subset data if subset is None: raise PublishValidationError("Instance is missing subset " "name: {0}".format(subset)) if not isinstance(subset, six.string_types): raise TypeError("Instance subset name must be string, " "got: {0} ({1})".format(subset, type(subset))) # Ensure is not empty subset if not subset: raise ValueError("Instance subset name is " "empty: {0}".format(subset)) # Validate subset characters if not validate_name(subset): raise ValueError("Instance subset name contains invalid " "characters: {0}".format(subset)) ================================================ FILE: openpype/hosts/maya/plugins/publish/validate_instancer_content.py ================================================ import maya.cmds as cmds import pyblish.api from openpype.hosts.maya.api import lib from openpype.pipeline.publish import ( PublishValidationError, OptionalPyblishPluginMixin ) class ValidateInstancerContent(pyblish.api.InstancePlugin, OptionalPyblishPluginMixin): """Validates that all meshes in the instance have object IDs. This skips a check on intermediate objects because we consider them not important. """ order = pyblish.api.ValidatorOrder label = 'Instancer Content' families = ['instancer'] optional = False def process(self, instance): if not self.is_active(instance.data): return error = False members = instance.data['setMembers'] export_members = instance.data['exactExportMembers'] self.log.debug("Contents {0}".format(members)) if not len(members) == len(cmds.ls(members, type="instancer")): self.log.error("Instancer can only contain instancers") error = True # TODO: Implement better check for particles are cached if not cmds.ls(export_members, type="nucleus"): self.log.error("Instancer must have a connected nucleus") error = True if not cmds.ls(export_members, type="cacheFile"): self.log.error("Instancer must be cached") error = True hidden = self.check_geometry_hidden(export_members) if not hidden: error = True self.log.error("Instancer input geometry must be hidden " "the scene. Invalid: {0}".format(hidden)) # Ensure all in one group parents = cmds.listRelatives(members, allParents=True, fullPath=True) or [] roots = list(set(cmds.ls(parents, assemblies=True, long=True))) if len(roots) > 1: self.log.error("Instancer should all be contained in a single " "group. Current roots: {0}".format(roots)) error = True if error: raise PublishValidationError( "Instancer Content is invalid. See log.") def check_geometry_hidden(self, export_members): # Ensure all instanced geometry is hidden shapes = cmds.ls(export_members, dag=True, shapes=True, noIntermediate=True) meshes = cmds.ls(shapes, type="mesh") visible = [node for node in meshes if lib.is_visible(node, displayLayer=False, intermediateObject=False)] if visible: return False return True ================================================ FILE: openpype/hosts/maya/plugins/publish/validate_instancer_frame_ranges.py ================================================ import os import re import pyblish.api from openpype.pipeline.publish import ( PublishValidationError, OptionalPyblishPluginMixin ) def is_cache_resource(resource): """Return whether resource is a cacheFile resource""" required = set(["maya", "node", "cacheFile"]) tags = resource.get("tags", []) return required.issubset(tags) def valdidate_files(files): for f in files: assert os.path.exists(f) assert f.endswith(".mcx") or f.endswith(".mcc") return True def filter_ticks(files): tick_files = set() ticks = set() for path in files: match = re.match(".+Tick([0-9]+).mcx$", os.path.basename(path)) if match: tick_files.add(path) num = match.group(1) ticks.add(int(num)) return tick_files, ticks class ValidateInstancerFrameRanges(pyblish.api.InstancePlugin, OptionalPyblishPluginMixin): """Validates all instancer particle systems are cached correctly. This means they should have the files/frames as required by the start-end frame (including handles). This also checks the files exist and checks the "ticks" (substeps) files. """ order = pyblish.api.ValidatorOrder label = 'Instancer Cache Frame Ranges' families = ['instancer'] optional = False @classmethod def get_invalid(cls, instance): import pyseq start_frame = instance.data.get("frameStart", 0) end_frame = instance.data.get("frameEnd", 0) required = range(int(start_frame), int(end_frame) + 1) invalid = list() resources = instance.data.get("resources", []) for resource in resources: if not is_cache_resource(resource): continue node = resource['node'] all_files = resource['files'][:] all_lookup = set(all_files) # The first file is usually the .xml description file. xml = all_files.pop(0) assert xml.endswith(".xml") # Ensure all files exist (including ticks) # The remainder file paths should be the .mcx or .mcc files valdidate_files(all_files) # Maya particle caches support substeps by saving out additional # files that end with a Tick60.mcx, Tick120.mcx, etc. suffix. # To avoid `pyseq` getting confused we filter those out and then # for each file (except the last frame) check that at least all # ticks exist. tick_files, ticks = filter_ticks(all_files) if tick_files: files = [f for f in all_files if f not in tick_files] else: files = all_files sequences = pyseq.get_sequences(files) if len(sequences) != 1: invalid.append(node) cls.log.warning("More than one sequence found? " "{0} {1}".format(node, files)) cls.log.warning("Found caches: {0}".format(sequences)) continue sequence = sequences[0] cls.log.debug("Found sequence: {0}".format(sequence)) start = sequence.start() end = sequence.end() if start > start_frame or end < end_frame: invalid.append(node) cls.log.warning("Sequence does not have enough " "frames: {0}-{1} (requires: {2}-{3})" "".format(start, end, start_frame, end_frame)) continue # Ensure all frames are present missing = set(sequence.missing()) if missing: required_missing = [x for x in required if x in missing] if required_missing: invalid.append(node) cls.log.warning("Sequence is missing required frames: " "{0}".format(required_missing)) continue # Ensure all tick files (substep) exist for the files in the folder # for the frames required by the time range. if ticks: ticks = list(sorted(ticks)) cls.log.debug("Found ticks: {0} " "(substeps: {1})".format(ticks, len(ticks))) # Check all frames except the last since we don't # require subframes after our time range. tick_check_frames = set(required[:-1]) # Check all frames for item in sequence: frame = item.frame if not frame: invalid.append(node) cls.log.error("Path is not a frame in sequence: " "{0}".format(item)) continue # Not required for our time range if frame not in tick_check_frames: continue path = item.path for num in ticks: base, ext = os.path.splitext(path) tick_file = base + "Tick{0}".format(num) + ext if tick_file not in all_lookup: invalid.append(node) cls.log.warning("Tick file found that is not " "in cache query filenames: " "{0}".format(tick_file)) return invalid def process(self, instance): if not self.is_active(instance.data): return invalid = self.get_invalid(instance) if invalid: self.log.error("Invalid nodes: {0}".format(invalid)) raise PublishValidationError( ("Invalid particle caches in instance. " "See logs for details.")) ================================================ FILE: openpype/hosts/maya/plugins/publish/validate_loaded_plugin.py ================================================ import os import pyblish.api import maya.cmds as cmds from openpype.pipeline.publish import ( RepairContextAction, PublishValidationError, OptionalPyblishPluginMixin ) class ValidateLoadedPlugin(pyblish.api.ContextPlugin, OptionalPyblishPluginMixin): """Ensure there are no unauthorized loaded plugins""" label = "Loaded Plugin" order = pyblish.api.ValidatorOrder host = ["maya"] actions = [RepairContextAction] optional = True @classmethod def get_invalid(cls, context): invalid = [] loaded_plugin = cmds.pluginInfo(query=True, listPlugins=True) # get variable from OpenPype settings whitelist_native_plugins = cls.whitelist_native_plugins authorized_plugins = cls.authorized_plugins or [] for plugin in loaded_plugin: if not whitelist_native_plugins and os.getenv('MAYA_LOCATION') \ in cmds.pluginInfo(plugin, query=True, path=True): continue if plugin not in authorized_plugins: invalid.append(plugin) return invalid def process(self, context): if not self.is_active(context.data): return invalid = self.get_invalid(context) if invalid: raise PublishValidationError( "Found forbidden plugin name: {}".format(", ".join(invalid)) ) @classmethod def repair(cls, context): """Unload forbidden plugins""" for plugin in cls.get_invalid(context): cmds.pluginInfo(plugin, edit=True, autoload=False) cmds.unloadPlugin(plugin, force=True) ================================================ FILE: openpype/hosts/maya/plugins/publish/validate_look_contents.py ================================================ import pyblish.api import openpype.hosts.maya.api.action from openpype.pipeline.publish import ( PublishValidationError, ValidateContentsOrder ) from maya import cmds # noqa class ValidateLookContents(pyblish.api.InstancePlugin): """Validate look instance contents Rules: * Look data must have `relationships` and `attributes` keys. * At least one relationship must be collection. * All relationship object sets at least have an ID value Tip: * When no node IDs are found on shadingEngines please save your scene and try again. """ order = ValidateContentsOrder families = ['look'] hosts = ['maya'] label = 'Look Data Contents' actions = [openpype.hosts.maya.api.action.SelectInvalidAction] def process(self, instance): """Process all the nodes in the instance""" if not instance[:]: raise PublishValidationError("Instance is empty") invalid = self.get_invalid(instance) if invalid: raise PublishValidationError("'{}' has invalid look " "content".format(instance.name)) @classmethod def get_invalid(cls, instance): """Get all invalid nodes""" # check if data has the right attributes and content attributes = cls.validate_lookdata_attributes(instance) # check the looks for ID looks = cls.validate_looks(instance) # check if file nodes have valid files files = cls.validate_files(instance) invalid = looks + attributes + files return invalid @classmethod def validate_lookdata_attributes(cls, instance): """Check if the lookData has the required attributes Args: instance """ invalid = set() keys = ["relationships", "attributes"] lookdata = instance.data["lookData"] for key in keys: if key not in lookdata: cls.log.error("Look Data has no key " "'{}'".format(key)) invalid.add(instance.name) # Validate at least one single relationship is collected if not lookdata["relationships"]: cls.log.error("Look '%s' has no " "`relationships`" % instance.name) invalid.add(instance.name) # Check if attributes are on a node with an ID, crucial for rebuild! for attr_changes in lookdata["attributes"]: if not attr_changes["uuid"] and not attr_changes["attributes"]: cls.log.error("Node '%s' has no cbId, please set the " "attributes to its children if it has any" % attr_changes["name"]) invalid.add(instance.name) return list(invalid) @classmethod def validate_looks(cls, instance): looks = instance.data["lookData"]["relationships"] invalid = [] for name, data in looks.items(): if not data["uuid"]: cls.log.error("Look '{}' has no UUID".format(name)) invalid.append(name) return invalid @classmethod def validate_files(cls, instance): invalid = [] resources = instance.data.get("resources", []) for resource in resources: files = resource["files"] if len(files) == 0: node = resource["node"] cls.log.error("File node '%s' uses no or non-existing " "files" % node) invalid.append(node) return invalid @classmethod def validate_renderer(cls, instance): # TODO: Rewrite this to be more specific and configurable renderer = cmds.getAttr( 'defaultRenderGlobals.currentRenderer').lower() do_maketx = instance.data.get("maketx", False) do_rstex = instance.data.get("rstex", False) processors = [] if do_maketx: processors.append('arnold') if do_rstex: processors.append('redshift') for processor in processors: if processor == renderer: continue else: cls.log.error("Converted texture does not match current renderer.") # noqa ================================================ FILE: openpype/hosts/maya/plugins/publish/validate_look_default_shaders_connections.py ================================================ from maya import cmds import pyblish.api from openpype.pipeline.publish import ( ValidateContentsOrder, RepairContextAction, PublishValidationError ) class ValidateLookDefaultShadersConnections(pyblish.api.ContextPlugin): """Validate default shaders in the scene have their default connections. For example the standardSurface1 or lambert1 (maya 2023 and before) could potentially be disconnected from the initialShadingGroup. As such it's not lambert1 that will be identified as the default shader which can have unpredictable results. To fix the default connections need to be made again. See the logs for more details on which connections are missing. """ order = pyblish.api.ValidatorOrder - 0.4999 families = ['look'] hosts = ['maya'] label = 'Look Default Shader Connections' actions = [RepairContextAction] # The default connections to check DEFAULTS = { "initialShadingGroup.surfaceShader": ["standardSurface1.outColor", "lambert1.outColor"], "initialParticleSE.surfaceShader": ["standardSurface1.outColor", "lambert1.outColor"], "initialParticleSE.volumeShader": ["particleCloud1.outColor"] } def process(self, context): if self.get_invalid(): raise PublishValidationError( "Default shaders in your scene do not have their " "default shader connections. Please repair them to continue." ) @classmethod def get_invalid(cls): # Process as usual invalid = list() for plug, valid_inputs in cls.DEFAULTS.items(): inputs = cmds.listConnections(plug, source=True, destination=False, plugs=True) or None if not inputs or inputs[0] not in valid_inputs: cls.log.error( "{0} is not connected to {1}. This can result in " "unexpected behavior. Please reconnect to continue." "".format(plug, " or ".join(valid_inputs)) ) invalid.append(plug) return invalid @classmethod def repair(cls, context): invalid = cls.get_invalid() for plug in invalid: valid_inputs = cls.DEFAULTS[plug] for valid_input in valid_inputs: if cmds.objExists(valid_input): cls.log.info( "Connecting {} -> {}".format(valid_input, plug) ) cmds.connectAttr(valid_input, plug, force=True) break ================================================ FILE: openpype/hosts/maya/plugins/publish/validate_look_id_reference_edits.py ================================================ from collections import defaultdict from maya import cmds import pyblish.api import openpype.hosts.maya.api.action from openpype.pipeline.publish import ( RepairAction, ValidateContentsOrder, PublishValidationError ) class ValidateLookIdReferenceEdits(pyblish.api.InstancePlugin): """Validate nodes in look have no reference edits to cbId. Note: This only validates the cbId edits on the referenced nodes that are used in the look. For example, a transform can have its cbId changed without being invalidated when it is not used in the look's assignment. """ order = ValidateContentsOrder families = ['look'] hosts = ['maya'] label = 'Look Id Reference Edits' actions = [openpype.hosts.maya.api.action.SelectInvalidAction, RepairAction] def process(self, instance): invalid = self.get_invalid(instance) if invalid: raise PublishValidationError("Invalid nodes %s" % (invalid,)) @staticmethod def get_invalid(instance): # Collect all referenced members references = defaultdict(set) relationships = instance.data["lookData"]["relationships"] for relationship in relationships.values(): for member in relationship['members']: node = member["name"] if cmds.referenceQuery(node, isNodeReferenced=True): ref = cmds.referenceQuery(node, referenceNode=True) references[ref].add(node) # Validate whether any has changes to 'cbId' attribute invalid = list() for ref, nodes in references.items(): edits = cmds.referenceQuery(editAttrs=True, editNodes=True, showDagPath=True, showNamespace=True, onReferenceNode=ref) for edit in edits: # Ensure it is an attribute ending with .cbId # thus also ignore just node edits (like parenting) if not edit.endswith(".cbId"): continue # Ensure the attribute is 'cbId' (and not a nested attribute) node, attr = edit.split(".", 1) if attr != "cbId": continue if node in nodes: invalid.append(node) return invalid @classmethod def repair(cls, instance): invalid = cls.get_invalid(instance) # Group invalid nodes by reference node references = defaultdict(set) for node in invalid: ref = cmds.referenceQuery(node, referenceNode=True) references[ref].add(node) # Remove the reference edits on the nodes per reference node for ref, nodes in references.items(): for node in nodes: # Somehow this only works if you run the the removal # per edit command. for command in ["addAttr", "connectAttr", "deleteAttr", "disconnectAttr", "setAttr"]: cmds.referenceEdit("{}.cbId".format(node), removeEdits=True, successfulEdits=True, failedEdits=True, editCommand=command, onReferenceNode=ref) ================================================ FILE: openpype/hosts/maya/plugins/publish/validate_look_no_default_shaders.py ================================================ from maya import cmds import pyblish.api import openpype.hosts.maya.api.action from openpype.pipeline.publish import ( ValidateContentsOrder, PublishValidationError ) class ValidateLookNoDefaultShaders(pyblish.api.InstancePlugin): """Validate if any node has a connection to a default shader. This checks whether the look has any members of: - lambert1 - initialShadingGroup - initialParticleSE - particleCloud1 If any of those is present it will raise an error. A look is not allowed to have any of the "default" shaders present in a scene as they can introduce problems when referenced (overriding local scene shaders). To fix this no shape nodes in the look must have any of default shaders applied. """ order = ValidateContentsOrder + 0.01 families = ['look'] hosts = ['maya'] label = 'Look No Default Shaders' actions = [openpype.hosts.maya.api.action.SelectInvalidAction] DEFAULT_SHADERS = {"lambert1", "initialShadingGroup", "initialParticleSE", "particleCloud1"} def process(self, instance): """Process all the nodes in the instance""" invalid = self.get_invalid(instance) if invalid: raise PublishValidationError("Invalid node relationships found: " "{0}".format(invalid)) @classmethod def get_invalid(cls, instance): invalid = set() for node in instance: # Get shading engine connections shaders = cmds.listConnections(node, type="shadingEngine") or [] # Check for any disallowed connections on *all* nodes if any(s in cls.DEFAULT_SHADERS for s in shaders): # Explicitly log each individual "wrong" connection. for s in shaders: if s in cls.DEFAULT_SHADERS: cls.log.error("Node has unallowed connection to " "'{}': {}".format(s, node)) invalid.add(node) return list(invalid) ================================================ FILE: openpype/hosts/maya/plugins/publish/validate_look_sets.py ================================================ import pyblish.api import openpype.hosts.maya.api.action from openpype.hosts.maya.api import lib from openpype.pipeline.publish import ( ValidateContentsOrder, PublishValidationError ) class ValidateLookSets(pyblish.api.InstancePlugin): """Validate if any sets relationships are not being collected. A shader can be assigned to a node that is missing a Colorbleed ID. Because it is missing the ID it has not been collected in the instance. This validator ensures those relationships and thus considers it invalid if a relationship was not collected. When the relationship needs to be maintained the artist might need to create a different* relationship or ensure the node has the Colorbleed ID. *The relationship might be too broad (assigned to top node of hierarchy). This can be countered by creating the relationship on the shape or its transform. In essence, ensure item the shader is assigned to has the Colorbleed ID! Examples: - Displacement objectSets (like V-Ray): It is best practice to add the transform of the shape to the displacement objectSet. Any parent groups will not work as groups do not receive a Colorbleed Id. As such the assignments need to be made to the shapes and their transform. Example content: [asset_GRP|geometry_GRP|body_GES, asset_GRP|geometry_GRP|L_eye_GES, asset_GRP|geometry_GRP|R_eye_GES, asset_GRP|geometry_GRP|wings_GEO] """ order = ValidateContentsOrder families = ['look'] hosts = ['maya'] label = 'Look Sets' actions = [openpype.hosts.maya.api.action.SelectInvalidAction] def process(self, instance): """Process all the nodes in the instance""" invalid = self.get_invalid(instance) if invalid: raise PublishValidationError("'{}' has invalid look " "content".format(instance.name)) @classmethod def get_invalid(cls, instance): """Get all invalid nodes""" relationships = instance.data["lookData"]["relationships"] invalid = [] renderlayer = instance.data.get("renderlayer", "defaultRenderLayer") with lib.renderlayer(renderlayer): for node in instance: # get the connected objectSets of the node sets = lib.get_related_sets(node) if not sets: continue # check if any objectSets are not present ion the relationships missing_sets = [s for s in sets if s not in relationships] if missing_sets: for missing_set in missing_sets: cls.log.debug(missing_set) if '_SET' not in missing_set: # A set of this node is not coming along, this is wrong! cls.log.error("Missing sets '{}' for node " "'{}'".format(missing_sets, node)) invalid.append(node) continue # Ensure the node is in the sets that are collected for shader_set, data in relationships.items(): if shader_set not in sets: # no need to check for a set if the node # isn't in it anyway continue member_nodes = [member['name'] for member in data['members']] if node not in member_nodes: # The node is not found in the collected set # relationships cls.log.error("Missing '{}' in collected set node " "'{}'".format(node, shader_set)) invalid.append(node) continue return invalid ================================================ FILE: openpype/hosts/maya/plugins/publish/validate_look_shading_group.py ================================================ from maya import cmds import pyblish.api import openpype.hosts.maya.api.action from openpype.pipeline.publish import ( RepairAction, ValidateContentsOrder, PublishValidationError, OptionalPyblishPluginMixin ) class ValidateShadingEngine(pyblish.api.InstancePlugin, OptionalPyblishPluginMixin): """Validate all shading engines are named after the surface material. Shading engines should be named "{surface_shader}SG" """ order = ValidateContentsOrder families = ["look"] hosts = ["maya"] label = "Look Shading Engine Naming" actions = [ openpype.hosts.maya.api.action.SelectInvalidAction, RepairAction ] optional = True # The default connections to check def process(self, instance): if not self.is_active(instance.data): return invalid = self.get_invalid(instance) if invalid: raise PublishValidationError( "Found shading engines with incorrect naming:" "\n{}".format(invalid) ) @classmethod def get_invalid(cls, instance): shapes = cmds.ls(instance, type=["nurbsSurface", "mesh"], long=True) invalid = [] for shape in shapes: shading_engines = cmds.listConnections( shape, destination=True, type="shadingEngine" ) or [] for shading_engine in shading_engines: name = ( cmds.listConnections(shading_engine + ".surfaceShader")[0] + "SG" ) if shading_engine != name: invalid.append(shading_engine) return list(set(invalid)) @classmethod def repair(cls, instance): shading_engines = cls.get_invalid(instance) for shading_engine in shading_engines: name = ( cmds.listConnections(shading_engine + ".surfaceShader")[0] + "SG" ) cmds.rename(shading_engine, name) ================================================ FILE: openpype/hosts/maya/plugins/publish/validate_look_single_shader.py ================================================ import pyblish.api from maya import cmds import openpype.hosts.maya.api.action from openpype.pipeline.publish import ( PublishValidationError, ValidateContentsOrder) class ValidateSingleShader(pyblish.api.InstancePlugin): """Validate all nurbsSurfaces and meshes have exactly one shader assigned. This will error if a shape has no shaders or more than one shader. """ order = ValidateContentsOrder families = ['look'] hosts = ['maya'] label = 'Look Single Shader Per Shape' actions = [openpype.hosts.maya.api.action.SelectInvalidAction] # The default connections to check def process(self, instance): invalid = self.get_invalid(instance) if invalid: raise PublishValidationError( ("Found shapes which don't have a single shader " "assigned:\n{}").format(invalid)) @classmethod def get_invalid(cls, instance): # Get all shapes from the instance shapes = cmds.ls(instance, type=["nurbsSurface", "mesh"], long=True) # Check the number of connected shadingEngines per shape no_shaders = [] more_than_one_shaders = [] for shape in shapes: shading_engines = cmds.listConnections(shape, destination=True, type="shadingEngine") or [] # Only interested in unique shading engines. shading_engines = list(set(shading_engines)) if not shading_engines: no_shaders.append(shape) elif len(shading_engines) > 1: more_than_one_shaders.append(shape) if no_shaders: cls.log.error("No shaders found on: {}".format(no_shaders)) if more_than_one_shaders: cls.log.error("More than one shader found on: " "{}".format(more_than_one_shaders)) return no_shaders + more_than_one_shaders ================================================ FILE: openpype/hosts/maya/plugins/publish/validate_maya_units.py ================================================ import maya.cmds as cmds import pyblish.api import openpype.hosts.maya.api.lib as mayalib from openpype.pipeline.context_tools import get_current_project_asset from openpype.pipeline.publish import ( RepairContextAction, ValidateSceneOrder, PublishXmlValidationError, OptionalPyblishPluginMixin ) class ValidateMayaUnits(pyblish.api.ContextPlugin, OptionalPyblishPluginMixin): """Check if the Maya units are set correct""" order = ValidateSceneOrder label = "Maya Units" hosts = ['maya'] actions = [RepairContextAction] validate_linear_units = True linear_units = "cm" validate_angular_units = True angular_units = "deg" validate_fps = True nice_message_format = ( "- {setting} must be {required_value}. " "Your scene is set to {current_value}" ) log_message_format = ( "Maya scene {setting} must be '{required_value}'. " "Current value is '{current_value}'." ) optional = False @classmethod def apply_settings(cls, project_settings): """Apply project settings to creator""" settings = ( project_settings["maya"]["publish"]["ValidateMayaUnits"] ) cls.validate_linear_units = settings.get("validate_linear_units", cls.validate_linear_units) cls.linear_units = settings.get("linear_units", cls.linear_units) cls.validate_angular_units = settings.get("validate_angular_units", cls.validate_angular_units) cls.angular_units = settings.get("angular_units", cls.angular_units) cls.validate_fps = settings.get("validate_fps", cls.validate_fps) def process(self, context): if not self.is_active(context.data): return # Collected units linearunits = context.data.get('linearUnits') angularunits = context.data.get('angularUnits') fps = context.data.get('fps') asset_doc = context.data["assetEntity"] asset_fps = mayalib.convert_to_maya_fps(asset_doc["data"]["fps"]) self.log.info('Units (linear): {0}'.format(linearunits)) self.log.info('Units (angular): {0}'.format(angularunits)) self.log.info('Units (time): {0} FPS'.format(fps)) invalid = [] # Check if units are correct if ( self.validate_linear_units and linearunits and linearunits != self.linear_units ): invalid.append({ "setting": "Linear units", "required_value": self.linear_units, "current_value": linearunits }) if ( self.validate_angular_units and angularunits and angularunits != self.angular_units ): invalid.append({ "setting": "Angular units", "required_value": self.angular_units, "current_value": angularunits }) if self.validate_fps and fps and fps != asset_fps: invalid.append({ "setting": "FPS", "required_value": asset_fps, "current_value": fps }) if invalid: issues = [] for data in invalid: self.log.error(self.log_message_format.format(**data)) issues.append(self.nice_message_format.format(**data)) issues = "\n".join(issues) raise PublishXmlValidationError( plugin=self, message="Invalid maya scene units", formatting_data={"issues": issues} ) @classmethod def repair(cls, context): """Fix the current FPS setting of the scene, set to PAL(25.0 fps)""" cls.log.info("Setting angular unit to '{}'".format(cls.angular_units)) cmds.currentUnit(angle=cls.angular_units) current_angle = cmds.currentUnit(query=True, angle=True) cls.log.debug(current_angle) cls.log.info("Setting linear unit to '{}'".format(cls.linear_units)) cmds.currentUnit(linear=cls.linear_units) current_linear = cmds.currentUnit(query=True, linear=True) cls.log.debug(current_linear) cls.log.info("Setting time unit to match project") # TODO replace query with using 'context.data["assetEntity"]' asset_doc = get_current_project_asset() asset_fps = asset_doc["data"]["fps"] mayalib.set_scene_fps(asset_fps) ================================================ FILE: openpype/hosts/maya/plugins/publish/validate_mesh_arnold_attributes.py ================================================ from maya import cmds import pyblish.api import openpype.hosts.maya.api.action from openpype.hosts.maya.api.lib import ( maintained_selection, delete_after, undo_chunk, get_attribute, set_attribute ) from openpype.pipeline.publish import ( OptionalPyblishPluginMixin, RepairAction, ValidateMeshOrder, PublishValidationError ) class ValidateMeshArnoldAttributes(pyblish.api.InstancePlugin, OptionalPyblishPluginMixin): """Validate the mesh has default Arnold attributes. It compares all Arnold attributes from a default mesh. This is to ensure later published looks can discover non-default Arnold attributes. """ order = ValidateMeshOrder hosts = ["maya"] families = ["model"] label = "Mesh Arnold Attributes" actions = [ openpype.hosts.maya.api.action.SelectInvalidAction, RepairAction ] optional = True # cache (will be `dict` when cached) arnold_mesh_defaults = None @classmethod def get_default_attributes(cls): if cls.arnold_mesh_defaults is not None: # Use from cache return cls.arnold_mesh_defaults # Get default arnold attribute values for mesh type. defaults = {} with delete_after() as tmp: transform = cmds.createNode("transform", skipSelect=True) tmp.append(transform) mesh = cmds.createNode("mesh", parent=transform, skipSelect=True) arnold_attributes = cmds.listAttr(mesh, string="ai*", fromPlugin=True) or [] for attr in arnold_attributes: plug = "{}.{}".format(mesh, attr) try: defaults[attr] = get_attribute(plug) except PublishValidationError: cls.log.debug("Ignoring arnold attribute: {}".format(attr)) cls.arnold_mesh_defaults = defaults # assign cache return defaults @classmethod def get_invalid_attributes(cls, instance, compute=False): invalid = [] if compute: meshes = cmds.ls(instance, type="mesh", long=True) if not meshes: return [] # Compare the values against the defaults defaults = cls.get_default_attributes() for mesh in meshes: for attr_name, default_value in defaults.items(): plug = "{}.{}".format(mesh, attr_name) if get_attribute(plug) != default_value: invalid.append(plug) instance.data["nondefault_arnold_attributes"] = invalid return instance.data.get("nondefault_arnold_attributes", []) @classmethod def get_invalid(cls, instance): invalid_attrs = cls.get_invalid_attributes(instance, compute=False) invalid_nodes = set(attr.split(".", 1)[0] for attr in invalid_attrs) return sorted(invalid_nodes) @classmethod def repair(cls, instance): with maintained_selection(): with undo_chunk(): defaults = cls.get_default_attributes() attributes = cls.get_invalid_attributes( instance, compute=False ) for attr in attributes: node, attr_name = attr.split(".", 1) value = defaults[attr_name] set_attribute( node=node, attribute=attr_name, value=value ) def process(self, instance): if not self.is_active(instance.data): return if not cmds.pluginInfo("mtoa", query=True, loaded=True): # Arnold attributes only exist if plug-in is loaded return invalid = self.get_invalid_attributes(instance, compute=True) if invalid: raise PublishValidationError( "Non-default Arnold attributes found in instance:" " {0}".format(invalid) ) ================================================ FILE: openpype/hosts/maya/plugins/publish/validate_mesh_empty.py ================================================ from maya import cmds import pyblish.api import openpype.hosts.maya.api.action from openpype.pipeline.publish import ( RepairAction, ValidateMeshOrder, PublishValidationError ) class ValidateMeshEmpty(pyblish.api.InstancePlugin): """Validate meshes have some vertices. Its possible to have meshes without any vertices. To replicate this issue, delete all faces/polygons then all edges. """ order = ValidateMeshOrder hosts = ["maya"] families = ["model"] label = "Mesh Empty" actions = [ openpype.hosts.maya.api.action.SelectInvalidAction, RepairAction ] @classmethod def repair(cls, instance): invalid = cls.get_invalid(instance) for node in invalid: cmds.delete(node) @classmethod def get_invalid(cls, instance): invalid = [] meshes = cmds.ls(instance, type="mesh", long=True) for mesh in meshes: num_vertices = cmds.polyEvaluate(mesh, vertex=True) if num_vertices == 0: cls.log.warning( "\"{}\" does not have any vertices.".format(mesh) ) invalid.append(mesh) return invalid def process(self, instance): invalid = self.get_invalid(instance) if invalid: raise PublishValidationError( "Meshes found in instance without any vertices: %s" % invalid ) ================================================ FILE: openpype/hosts/maya/plugins/publish/validate_mesh_has_uv.py ================================================ from maya import cmds import pyblish.api import openpype.hosts.maya.api.action from openpype.pipeline.publish import ( ValidateMeshOrder, OptionalPyblishPluginMixin, PublishValidationError ) from openpype.hosts.maya.api.lib import len_flattened class ValidateMeshHasUVs(pyblish.api.InstancePlugin, OptionalPyblishPluginMixin): """Validate the current mesh has UVs. It validates whether the current UV set has non-zero UVs and at least more than the vertex count. It's not really bulletproof, but a simple quick validation to check if there are likely UVs for every face. """ order = ValidateMeshOrder hosts = ['maya'] families = ['model'] label = 'Mesh Has UVs' actions = [openpype.hosts.maya.api.action.SelectInvalidAction] optional = True @classmethod def get_invalid(cls, instance): invalid = [] for node in cmds.ls(instance, type='mesh'): num_vertices = cmds.polyEvaluate(node, vertex=True) if num_vertices == 0: cls.log.warning( "Skipping \"{}\", cause it does not have any " "vertices.".format(node) ) continue uv = cmds.polyEvaluate(node, uv=True) if uv == 0: invalid.append(node) continue vertex = cmds.polyEvaluate(node, vertex=True) if uv < vertex: # Workaround: # Maya can have instanced UVs in a single mesh, for example # imported from an Alembic. With instanced UVs the UV count # from `maya.cmds.polyEvaluate(uv=True)` will only result in # the unique UV count instead of for all vertices. # # Note: Maya can save instanced UVs to `mayaAscii` but cannot # load this as instanced. So saving, opening and saving # again will lose this information. map_attr = "{}.map[*]".format(node) uv_to_vertex = cmds.polyListComponentConversion(map_attr, toVertex=True) uv_vertex_count = len_flattened(uv_to_vertex) if uv_vertex_count < vertex: invalid.append(node) else: cls.log.warning("Node has instanced UV points: " "{0}".format(node)) return invalid def process(self, instance): if not self.is_active(instance.data): return invalid = self.get_invalid(instance) if invalid: names = " Skeletal Mesh Top Node ## Skeletal meshes needs common root Skeletal meshes and their joints must be under one common root. ### How to repair? Make sure all geometry and joints resides under same root.
".join( " - {}".format(node) for node in invalid ) raise PublishValidationError( title="Mesh has missing UVs", message="Model meshes are required to have UVs.
" "Meshes detected with invalid or missing UVs:
" "{0}".format(names) ) ================================================ FILE: openpype/hosts/maya/plugins/publish/validate_mesh_lamina_faces.py ================================================ from maya import cmds import pyblish.api import openpype.hosts.maya.api.action from openpype.pipeline.publish import ( ValidateMeshOrder, OptionalPyblishPluginMixin ) class ValidateMeshLaminaFaces(pyblish.api.InstancePlugin, OptionalPyblishPluginMixin): """Validate meshes don't have lamina faces. Lamina faces share all of their edges. """ order = ValidateMeshOrder hosts = ['maya'] families = ['model'] label = 'Mesh Lamina Faces' actions = [openpype.hosts.maya.api.action.SelectInvalidAction] optional = True @staticmethod def get_invalid(instance): meshes = cmds.ls(instance, type='mesh', long=True) invalid = [mesh for mesh in meshes if cmds.polyInfo(mesh, laminaFaces=True)] return invalid def process(self, instance): """Process all the nodes in the instance 'objectSet'""" if not self.is_active(instance.data): return invalid = self.get_invalid(instance) if invalid: raise ValueError("Meshes found with lamina faces: " "{0}".format(invalid)) ================================================ FILE: openpype/hosts/maya/plugins/publish/validate_mesh_ngons.py ================================================ from maya import cmds import pyblish.api import openpype.hosts.maya.api.action from openpype.hosts.maya.api import lib from openpype.pipeline.publish import ( ValidateContentsOrder, OptionalPyblishPluginMixin ) class ValidateMeshNgons(pyblish.api.Validator, OptionalPyblishPluginMixin): """Ensure that meshes don't have ngons Ngon are faces with more than 4 sides. To debug the problem on the meshes you can use Maya's modeling tool: "Mesh > Cleanup..." """ order = ValidateContentsOrder hosts = ["maya"] families = ["model"] label = "Mesh ngons" actions = [openpype.hosts.maya.api.action.SelectInvalidAction] optional = True @staticmethod def get_invalid(instance): meshes = cmds.ls(instance, type='mesh', long=True) # Get all faces faces = ['{0}.f[*]'.format(node) for node in meshes] # Filter to n-sided polygon faces (ngons) invalid = lib.polyConstraint(faces, t=0x0008, # type=face size=3) # size=nsided return invalid def process(self, instance): """Process all the nodes in the instance "objectSet""" if not self.is_active(instance.data): return invalid = self.get_invalid(instance) if invalid: raise ValueError("Meshes found with n-gons" "values: {0}".format(invalid)) ================================================ FILE: openpype/hosts/maya/plugins/publish/validate_mesh_no_negative_scale.py ================================================ from maya import cmds import pyblish.api import openpype.hosts.maya.api.action from openpype.pipeline.publish import ( ValidateMeshOrder, PublishValidationError, OptionalPyblishPluginMixin ) def _as_report_list(values, prefix="- ", suffix="\n"): """Return list as bullet point list for a report""" if not values: return "" return prefix + (suffix + prefix).join(values) class ValidateMeshNoNegativeScale(pyblish.api.Validator, OptionalPyblishPluginMixin): """Ensure that meshes don't have a negative scale. Using negatively scaled proxies in a VRayMesh results in inverted normals. As such we want to avoid this. We also avoid this on the rig or model because these are often the previous steps for those that are cached to proxies so we can catch this issue early. """ order = ValidateMeshOrder hosts = ['maya'] families = ['model'] label = 'Mesh No Negative Scale' actions = [openpype.hosts.maya.api.action.SelectInvalidAction] optional = False @staticmethod def get_invalid(instance): meshes = cmds.ls(instance, type='mesh', long=True, noIntermediate=True) invalid = [] for mesh in meshes: transform = cmds.listRelatives(mesh, parent=True, fullPath=True)[0] scale = cmds.getAttr("{0}.scale".format(transform))[0] if any(x < 0 for x in scale): invalid.append(mesh) return invalid def process(self, instance): """Process all the nodes in the instance 'objectSet'""" if not self.is_active(instance.data): return invalid = self.get_invalid(instance) if invalid: raise PublishValidationError( "Meshes found with negative scale:\n\n{0}".format( _as_report_list(sorted(invalid)) ), title="Negative scale" ) ================================================ FILE: openpype/hosts/maya/plugins/publish/validate_mesh_non_manifold.py ================================================ from maya import cmds import pyblish.api import openpype.hosts.maya.api.action from openpype.pipeline.publish import ( ValidateMeshOrder, PublishValidationError, OptionalPyblishPluginMixin ) def _as_report_list(values, prefix="- ", suffix="\n"): """Return list as bullet point list for a report""" if not values: return "" return prefix + (suffix + prefix).join(values) class ValidateMeshNonManifold(pyblish.api.Validator, OptionalPyblishPluginMixin): """Ensure that meshes don't have non-manifold edges or vertices To debug the problem on the meshes you can use Maya's modeling tool: "Mesh > Cleanup..." """ order = ValidateMeshOrder hosts = ['maya'] families = ['model'] label = 'Mesh Non-Manifold Edges/Vertices' actions = [openpype.hosts.maya.api.action.SelectInvalidAction] optional = True @staticmethod def get_invalid(instance): meshes = cmds.ls(instance, type='mesh', long=True) invalid = [] for mesh in meshes: if (cmds.polyInfo(mesh, nonManifoldVertices=True) or cmds.polyInfo(mesh, nonManifoldEdges=True)): invalid.append(mesh) return invalid def process(self, instance): """Process all the nodes in the instance 'objectSet'""" if not self.is_active(instance.data): return invalid = self.get_invalid(instance) if invalid: raise PublishValidationError( "Meshes found with non-manifold edges/vertices:\n\n{0}".format( _as_report_list(sorted(invalid)) ), title="Non-Manifold Edges/Vertices" ) ================================================ FILE: openpype/hosts/maya/plugins/publish/validate_mesh_non_zero_edge.py ================================================ from maya import cmds import pyblish.api import openpype.hosts.maya.api.action from openpype.hosts.maya.api import lib from openpype.pipeline.publish import ( ValidateMeshOrder, OptionalPyblishPluginMixin, PublishValidationError ) class ValidateMeshNonZeroEdgeLength(pyblish.api.InstancePlugin, OptionalPyblishPluginMixin): """Validate meshes don't have edges with a zero length. Based on Maya's polyCleanup 'Edges with zero length'. Note: This can be slow for high-res meshes. """ order = ValidateMeshOrder families = ['model'] hosts = ['maya'] label = 'Mesh Edge Length Non Zero' actions = [openpype.hosts.maya.api.action.SelectInvalidAction] optional = True __tolerance = 1e-5 @classmethod def get_invalid(cls, instance): """Return the invalid edges. Also see: http://help.autodesk.com/view/MAYAUL/2015/ENU/?guid=Mesh__Cleanup """ meshes = cmds.ls(instance, type='mesh', long=True) if not meshes: return list() valid_meshes = [] for mesh in meshes: num_vertices = cmds.polyEvaluate(mesh, vertex=True) if num_vertices == 0: cls.log.warning( "Skipping \"{}\", cause it does not have any " "vertices.".format(mesh) ) continue valid_meshes.append(mesh) # Get all edges edges = ['{0}.e[*]'.format(node) for node in valid_meshes] # Filter by constraint on edge length invalid = lib.polyConstraint(edges, t=0x8000, # type=edge length=1, lengthbound=(0, cls.__tolerance)) return invalid def process(self, instance): """Process all meshes""" if not self.is_active(instance.data): return invalid = self.get_invalid(instance) if invalid: label = "Meshes found with zero edge length" raise PublishValidationError( message="{}: {}".format(label, invalid), title=label, description="{}:\n- ".format(label) + "\n- ".join(invalid) ) ================================================ FILE: openpype/hosts/maya/plugins/publish/validate_mesh_normals_unlocked.py ================================================ from maya import cmds import maya.api.OpenMaya as om2 import pyblish.api import openpype.hosts.maya.api.action from openpype.pipeline.publish import ( RepairAction, ValidateMeshOrder, OptionalPyblishPluginMixin, PublishValidationError ) def _as_report_list(values, prefix="- ", suffix="\n"): """Return list as bullet point list for a report""" if not values: return "" return prefix + (suffix + prefix).join(values) class ValidateMeshNormalsUnlocked(pyblish.api.Validator, OptionalPyblishPluginMixin): """Validate all meshes in the instance have unlocked normals These can be unlocked manually through: Modeling > Mesh Display > Unlock Normals """ order = ValidateMeshOrder hosts = ['maya'] families = ['model'] label = 'Mesh Normals Unlocked' actions = [openpype.hosts.maya.api.action.SelectInvalidAction, RepairAction] optional = True @staticmethod def has_locked_normals(mesh): """Return whether mesh has at least one locked normal""" sel = om2.MGlobal.getSelectionListByName(mesh) node = sel.getDependNode(0) fn_mesh = om2.MFnMesh(node) _, normal_ids = fn_mesh.getNormalIds() for normal_id in normal_ids: if fn_mesh.isNormalLocked(normal_id): return True return False @classmethod def get_invalid(cls, instance): """Return the meshes with locked normals in instance""" meshes = cmds.ls(instance, type='mesh', long=True) return [mesh for mesh in meshes if cls.has_locked_normals(mesh)] def process(self, instance): """Raise invalid when any of the meshes have locked normals""" if not self.is_active(instance.data): return invalid = self.get_invalid(instance) if invalid: raise PublishValidationError( "Meshes found with locked normals:\n\n{0}".format( _as_report_list(sorted(invalid)) ), title="Locked normals" ) @classmethod def repair(cls, instance): """Unlocks all normals on the meshes in this instance.""" invalid = cls.get_invalid(instance) for mesh in invalid: cmds.polyNormalPerVertex(mesh, unFreezeNormal=True) ================================================ FILE: openpype/hosts/maya/plugins/publish/validate_mesh_overlapping_uvs.py ================================================ import math from six.moves import xrange from maya import cmds import maya.api.OpenMaya as om import pyblish.api import openpype.hosts.maya.api.action from openpype.pipeline.publish import ( ValidateMeshOrder, OptionalPyblishPluginMixin, PublishValidationError ) def _as_report_list(values, prefix="- ", suffix="\n"): """Return list as bullet point list for a report""" if not values: return "" return prefix + (suffix + prefix).join(values) class GetOverlappingUVs(object): def _createBoundingCircle(self, meshfn): """ Represent a face by center and radius :param meshfn: MFnMesh class :type meshfn: :class:`maya.api.OpenMaya.MFnMesh` :returns: (center, radius) :rtype: tuple """ center = [] radius = [] for i in xrange(meshfn.numPolygons): # noqa: F821 # get uvs from face uarray = [] varray = [] for j in range(len(meshfn.getPolygonVertices(i))): uv = meshfn.getPolygonUV(i, j) uarray.append(uv[0]) varray.append(uv[1]) # loop through all vertices to construct edges/rays cu = 0.0 cv = 0.0 for j in range(len(uarray)): cu += uarray[j] cv += varray[j] cu /= len(uarray) cv /= len(varray) rsqr = 0.0 for j in range(len(varray)): du = uarray[j] - cu dv = varray[j] - cv dsqr = du * du + dv * dv rsqr = dsqr if dsqr > rsqr else rsqr center.append(cu) center.append(cv) radius.append(math.sqrt(rsqr)) return center, radius def _createRayGivenFace(self, meshfn, faceId): """ Represent a face by a series of edges(rays), i.e. :param meshfn: MFnMesh class :type meshfn: :class:`maya.api.OpenMaya.MFnMesh` :param faceId: face id :type faceId: int :returns: False if no valid uv's. ""(True, orig, vec)"" or ""(False, None, None)"" :rtype: tuple .. code-block:: python orig = [orig1u, orig1v, orig2u, orig2v, ... ] vec = [vec1u, vec1v, vec2u, vec2v, ... ] """ orig = [] vec = [] # get uvs uarray = [] varray = [] for i in range(len(meshfn.getPolygonVertices(faceId))): uv = meshfn.getPolygonUV(faceId, i) uarray.append(uv[0]) varray.append(uv[1]) if len(uarray) == 0 or len(varray) == 0: return (False, None, None) # loop through all vertices to construct edges/rays u = uarray[-1] v = varray[-1] for i in xrange(len(uarray)): # noqa: F821 orig.append(uarray[i]) orig.append(varray[i]) vec.append(u - uarray[i]) vec.append(v - varray[i]) u = uarray[i] v = varray[i] return (True, orig, vec) def _checkCrossingEdges(self, face1Orig, face1Vec, face2Orig, face2Vec): """ Check if there are crossing edges between two faces. Return True if there are crossing edges and False otherwise. :param face1Orig: origin of face 1 :type face1Orig: tuple :param face1Vec: face 1 edges :type face1Vec: list :param face2Orig: origin of face 2 :type face2Orig: tuple :param face2Vec: face 2 edges :type face2Vec: list A face is represented by a series of edges(rays), i.e. .. code-block:: python faceOrig[] = [orig1u, orig1v, orig2u, orig2v, ... ] faceVec[] = [vec1u, vec1v, vec2u, vec2v, ... ] """ face1Size = len(face1Orig) face2Size = len(face2Orig) for i in xrange(0, face1Size, 2): # noqa: F821 o1x = face1Orig[i] o1y = face1Orig[i+1] v1x = face1Vec[i] v1y = face1Vec[i+1] n1x = v1y n1y = -v1x for j in xrange(0, face2Size, 2): # noqa: F821 # Given ray1(O1, V1) and ray2(O2, V2) # Normal of ray1 is (V1.y, V1.x) o2x = face2Orig[j] o2y = face2Orig[j+1] v2x = face2Vec[j] v2y = face2Vec[j+1] n2x = v2y n2y = -v2x # Find t for ray2 # t = [(o1x-o2x)n1x + (o1y-o2y)n1y] / # (v2x * n1x + v2y * n1y) denum = v2x * n1x + v2y * n1y # Edges are parallel if denum is close to 0. if math.fabs(denum) < 0.000001: continue t2 = ((o1x-o2x) * n1x + (o1y-o2y) * n1y) / denum if (t2 < 0.00001 or t2 > 0.99999): continue # Find t for ray1 # t = [(o2x-o1x)n2x # + (o2y-o1y)n2y] / (v1x * n2x + v1y * n2y) denum = v1x * n2x + v1y * n2y # Edges are parallel if denum is close to 0. if math.fabs(denum) < 0.000001: continue t1 = ((o2x-o1x) * n2x + (o2y-o1y) * n2y) / denum # Edges intersect if (t1 > 0.00001 and t1 < 0.99999): return 1 return 0 def _getOverlapUVFaces(self, meshName): """ Return overlapping faces :param meshName: name of mesh :type meshName: str :returns: list of overlapping faces :rtype: list """ faces = [] # find polygon mesh node selList = om.MSelectionList() selList.add(meshName) mesh = selList.getDependNode(0) if mesh.apiType() == om.MFn.kTransform: dagPath = selList.getDagPath(0) dagFn = om.MFnDagNode(dagPath) child = dagFn.child(0) if child.apiType() != om.MFn.kMesh: raise Exception("Can't find polygon mesh") mesh = child meshfn = om.MFnMesh(mesh) center, radius = self._createBoundingCircle(meshfn) for i in xrange(meshfn.numPolygons): # noqa: F821 rayb1, face1Orig, face1Vec = self._createRayGivenFace(meshfn, i) if not rayb1: continue cui = center[2*i] cvi = center[2*i+1] ri = radius[i] # Exclude the degenerate face # if(area(face1Orig) < 0.000001) continue; # Loop through face j where j != i for j in range(i+1, meshfn.numPolygons): cuj = center[2*j] cvj = center[2*j+1] rj = radius[j] du = cuj - cui dv = cvj - cvi dsqr = du * du + dv * dv # Quick rejection if bounding circles don't overlap if (dsqr >= (ri + rj) * (ri + rj)): continue rayb2, face2Orig, face2Vec = self._createRayGivenFace(meshfn, j) if not rayb2: continue # Exclude the degenerate face # if(area(face2Orig) < 0.000001): continue; if self._checkCrossingEdges(face1Orig, face1Vec, face2Orig, face2Vec): face1 = '%s.f[%d]' % (meshfn.name(), i) face2 = '%s.f[%d]' % (meshfn.name(), j) if face1 not in faces: faces.append(face1) if face2 not in faces: faces.append(face2) return faces class ValidateMeshHasOverlappingUVs(pyblish.api.InstancePlugin, OptionalPyblishPluginMixin): """ Validate the current mesh overlapping UVs. It validates whether the current UVs are overlapping or not. It is optional to warn publisher about it. """ order = ValidateMeshOrder hosts = ['maya'] families = ['model'] label = 'Mesh Has Overlapping UVs' actions = [openpype.hosts.maya.api.action.SelectInvalidAction] optional = True @classmethod def _get_overlapping_uvs(cls, mesh): """Return overlapping UVs of mesh. Args: mesh (str): Mesh node name Returns: list: Overlapping uvs for the input mesh in all uv sets. """ ovl = GetOverlappingUVs() # Store original uv set original_current_uv_set = cmds.polyUVSet(mesh, query=True, currentUVSet=True)[0] overlapping_faces = [] for uv_set in cmds.polyUVSet(mesh, query=True, allUVSets=True): cmds.polyUVSet(mesh, currentUVSet=True, uvSet=uv_set) overlapping_faces.extend(ovl._getOverlapUVFaces(mesh)) # Restore original uv set cmds.polyUVSet(mesh, currentUVSet=True, uvSet=original_current_uv_set) return overlapping_faces @classmethod def get_invalid(cls, instance, compute=False): if compute: invalid = [] for node in cmds.ls(instance, type="mesh"): faces = cls._get_overlapping_uvs(node) invalid.extend(faces) instance.data["overlapping_faces"] = invalid return instance.data.get("overlapping_faces", []) def process(self, instance): if not self.is_active(instance.data): return invalid = self.get_invalid(instance, compute=True) if invalid: raise PublishValidationError( "Meshes found with overlapping UVs:\n\n{0}".format( _as_report_list(sorted(invalid)) ), title="Overlapping UVs" ) ================================================ FILE: openpype/hosts/maya/plugins/publish/validate_mesh_shader_connections.py ================================================ from maya import cmds import pyblish.api import openpype.hosts.maya.api.action from openpype.pipeline.publish import ( RepairAction, ValidateMeshOrder, PublishValidationError, OptionalPyblishPluginMixin ) def pairs(iterable): """Iterate over iterable per group of two""" a = iter(iterable) for i, y in zip(a, a): yield i, y def get_invalid_sets(shapes): """Return invalid sets for the given shapes. This takes a list of shape nodes to cache the set members for overlapping sets in the queries. This avoids many Maya set member queries. Returns: dict: Dictionary of shapes and their invalid sets, e.g. {"pCubeShape": ["set1", "set2"]} """ cache = dict() invalid = dict() # Collect the sets from the shape for shape in shapes: invalid_sets = [] sets = cmds.listSets(object=shape, t=1, extendToShape=False) or [] for set_ in sets: members = cache.get(set_, None) if members is None: members = set(cmds.ls(cmds.sets(set_, query=True, nodesOnly=True), long=True)) cache[set_] = members # If the shape is not actually present as a member of the set # consider it invalid if shape not in members: invalid_sets.append(set_) if invalid_sets: invalid[shape] = invalid_sets return invalid def disconnect(node_a, node_b): """Remove all connections between node a and b.""" # Disconnect outputs outputs = cmds.listConnections(node_a, plugs=True, connections=True, source=False, destination=True) for output, destination in pairs(outputs): if destination.split(".", 1)[0] == node_b: cmds.disconnectAttr(output, destination) # Disconnect inputs inputs = cmds.listConnections(node_a, plugs=True, connections=True, source=True, destination=False) for input, source in pairs(inputs): if source.split(".", 1)[0] == node_b: cmds.disconnectAttr(source, input) class ValidateMeshShaderConnections(pyblish.api.InstancePlugin, OptionalPyblishPluginMixin): """Ensure mesh shading engine connections are valid. In some scenarios Maya keeps connections to multiple shaders even if just a single one is assigned on the shape. These are related sets returned by `maya.cmds.listSets` that don't actually have the shape as member. """ order = ValidateMeshOrder hosts = ['maya'] families = ['model'] label = "Mesh Shader Connections" actions = [openpype.hosts.maya.api.action.SelectInvalidAction, RepairAction] optional = True def process(self, instance): """Process all the nodes in the instance 'objectSet'""" if not self.is_active(instance.data): return invalid = self.get_invalid(instance) if invalid: raise PublishValidationError("Shapes found with invalid shader " "connections: {0}".format(invalid)) @staticmethod def get_invalid(instance): nodes = instance[:] shapes = cmds.ls(nodes, noIntermediate=True, long=True, type="mesh") invalid = get_invalid_sets(shapes).keys() return invalid @classmethod def repair(cls, instance): shapes = cls.get_invalid(instance) invalid = get_invalid_sets(shapes) for shape, invalid_sets in invalid.items(): for set_node in invalid_sets: disconnect(shape, set_node) ================================================ FILE: openpype/hosts/maya/plugins/publish/validate_mesh_single_uv_set.py ================================================ from maya import cmds import pyblish.api import openpype.hosts.maya.api.action from openpype.hosts.maya.api import lib from openpype.pipeline.publish import ( RepairAction, ValidateMeshOrder, OptionalPyblishPluginMixin ) class ValidateMeshSingleUVSet(pyblish.api.InstancePlugin, OptionalPyblishPluginMixin): """Warn on multiple UV sets existing for each polygon mesh. On versions prior to Maya 2017 this will force no multiple uv sets because the Alembic exports in Maya prior to 2017 don't support writing multiple UV sets. """ order = ValidateMeshOrder hosts = ['maya'] families = ['model', 'pointcache'] optional = True label = "Mesh Single UV Set" actions = [openpype.hosts.maya.api.action.SelectInvalidAction, RepairAction] @staticmethod def get_invalid(instance): meshes = cmds.ls(instance, type='mesh', long=True) invalid = [] for mesh in meshes: uvSets = cmds.polyUVSet(mesh, query=True, allUVSets=True) or [] # ensure unique (sometimes maya will list 'map1' twice) uvSets = set(uvSets) if len(uvSets) != 1: invalid.append(mesh) return invalid def process(self, instance): """Process all the nodes in the instance 'objectSet'""" if not self.is_active(instance.data): return invalid = self.get_invalid(instance) if invalid: message = "Nodes found with multiple UV sets: {0}".format(invalid) # Maya 2017 and up allows multiple UV sets in Alembic exports # so we allow it, yet just warn the user to ensure they know about # the other UV sets. allowed = int(cmds.about(version=True)) >= 2017 if allowed: self.log.warning(message) else: raise ValueError(message) @classmethod def repair(cls, instance): for mesh in cls.get_invalid(instance): lib.remove_other_uv_sets(mesh) ================================================ FILE: openpype/hosts/maya/plugins/publish/validate_mesh_uv_set_map1.py ================================================ from maya import cmds import pyblish.api import openpype.hosts.maya.api.action from openpype.pipeline.publish import ( RepairAction, ValidateMeshOrder, OptionalPyblishPluginMixin ) class ValidateMeshUVSetMap1(pyblish.api.InstancePlugin, OptionalPyblishPluginMixin): """Validate model's default set exists and is named 'map1'. In Maya meshes by default have a uv set named "map1" that cannot be deleted. It can be renamed however, introducing some issues with some renderers. As such we ensure the first (default) UV set index is named "map1". """ order = ValidateMeshOrder hosts = ['maya'] families = ['model'] optional = True label = "Mesh has map1 UV Set" actions = [openpype.hosts.maya.api.action.SelectInvalidAction, RepairAction] @staticmethod def get_invalid(instance): meshes = cmds.ls(instance, type='mesh', long=True) invalid = [] for mesh in meshes: # Get existing mapping of uv sets by index indices = cmds.polyUVSet(mesh, query=True, allUVSetsIndices=True) maps = cmds.polyUVSet(mesh, query=True, allUVSets=True) mapping = dict(zip(indices, maps)) # Get the uv set at index zero. name = mapping[0] if name != "map1": invalid.append(mesh) return invalid def process(self, instance): """Process all the nodes in the instance 'objectSet'""" if not self.is_active(instance.data): return invalid = self.get_invalid(instance) if invalid: raise ValueError("Meshes found without 'map1' " "UV set: {0}".format(invalid)) @classmethod def repair(cls, instance): """Rename uv map at index zero to map1""" for mesh in cls.get_invalid(instance): # Get existing mapping of uv sets by index indices = cmds.polyUVSet(mesh, query=True, allUVSetsIndices=True) maps = cmds.polyUVSet(mesh, query=True, allUVSets=True) mapping = dict(zip(indices, maps)) # Ensure there is no uv set named map1 to avoid # a clash on renaming the "default uv set" to map1 existing = set(maps) if "map1" in existing: # Find a unique name index i = 2 while True: name = "map{0}".format(i) if name not in existing: break i += 1 cls.log.warning("Renaming clashing uv set name on mesh" " %s to '%s'", mesh, name) cmds.polyUVSet(mesh, rename=True, uvSet="map1", newUVSet=name) # Rename the initial index to map1 original = mapping[0] cmds.polyUVSet(mesh, rename=True, uvSet=original, newUVSet="map1") ================================================ FILE: openpype/hosts/maya/plugins/publish/validate_mesh_vertices_have_edges.py ================================================ import pyblish.api from maya import cmds import openpype.hosts.maya.api.action from openpype.hosts.maya.api.lib import len_flattened from openpype.pipeline.publish import ( PublishValidationError, RepairAction, ValidateMeshOrder, OptionalPyblishPluginMixin ) class ValidateMeshVerticesHaveEdges(pyblish.api.InstancePlugin, OptionalPyblishPluginMixin): """Validate meshes have only vertices that are connected to edges. Maya can have invalid geometry with vertices that have no edges or faces connected to them. In Maya 2016 EXT 2 and later there's a command to fix this: `maya.cmds.polyClean(mesh, cleanVertices=True)` In older versions of Maya it works to select the invalid vertices and merge the components. To find these invalid vertices select all vertices of the mesh that are visible in the viewport (drag to select), afterwards invert your selection (Ctrl + Shift + I). The remaining selection contains the invalid vertices. """ order = ValidateMeshOrder hosts = ['maya'] families = ['model'] label = 'Mesh Vertices Have Edges' actions = [openpype.hosts.maya.api.action.SelectInvalidAction, RepairAction] optional = True @classmethod def repair(cls, instance): # This fix only works in Maya 2016 EXT2 and newer if float(cmds.about(version=True)) <= 2016.0: raise PublishValidationError( ("Repair not supported in Maya version below " "2016 EXT 2")) invalid = cls.get_invalid(instance) for node in invalid: cmds.polyClean(node, cleanVertices=True) @classmethod def get_invalid(cls, instance): invalid = [] meshes = cmds.ls(instance, type="mesh", long=True) for mesh in meshes: num_vertices = cmds.polyEvaluate(mesh, vertex=True) if num_vertices == 0: cls.log.warning( "Skipping \"{}\", cause it does not have any " "vertices.".format(mesh) ) continue # Vertices from all edges edges = "%s.e[*]" % mesh vertices = cmds.polyListComponentConversion(edges, toVertex=True) num_vertices_from_edges = len_flattened(vertices) if num_vertices != num_vertices_from_edges: invalid.append(mesh) return invalid def process(self, instance): if not self.is_active(instance.data): return invalid = self.get_invalid(instance) if invalid: raise PublishValidationError( ("Meshes found in instance with vertices that " "have no edges: {}").format(invalid)) ================================================ FILE: openpype/hosts/maya/plugins/publish/validate_model_content.py ================================================ from maya import cmds import pyblish.api import openpype.hosts.maya.api.action from openpype.hosts.maya.api import lib from openpype.pipeline.publish import ( ValidateContentsOrder, PublishValidationError, OptionalPyblishPluginMixin ) class ValidateModelContent(pyblish.api.InstancePlugin, OptionalPyblishPluginMixin): """Adheres to the content of 'model' product type - Must have one top group. (configurable) - Must only contain: transforms, meshes and groups """ order = ValidateContentsOrder hosts = ["maya"] families = ["model"] label = "Model Content" actions = [openpype.hosts.maya.api.action.SelectInvalidAction] validate_top_group = True optional = False @classmethod def get_invalid(cls, instance): content_instance = instance.data.get("setMembers", None) if not content_instance: cls.log.error("Instance has no nodes!") return [instance.data["name"]] # All children will be included in the extracted export so we also # validate *all* descendents of the set members and we skip any # intermediate shapes descendants = cmds.listRelatives(content_instance, allDescendents=True, fullPath=True) or [] descendants = cmds.ls(descendants, noIntermediate=True, long=True) content_instance = list(set(content_instance + descendants)) # Ensure only valid node types allowed = ('mesh', 'transform', 'nurbsCurve', 'nurbsSurface', 'locator') nodes = cmds.ls(content_instance, long=True) valid = cmds.ls(content_instance, long=True, type=allowed) invalid = set(nodes) - set(valid) if invalid: cls.log.error("These nodes are not allowed: %s" % invalid) return list(invalid) if not valid: cls.log.error("No valid nodes in the instance") return True # Ensure it has shapes shapes = cmds.ls(valid, long=True, shapes=True) if not shapes: cls.log.error("No shapes in the model instance") return True # Top group top_parents = set([x.split("|")[1] for x in content_instance]) if cls.validate_top_group and len(top_parents) != 1: cls.log.error("Must have exactly one top group") return top_parents def _is_visible(node): """Return whether node is visible""" return lib.is_visible(node, displayLayer=False, intermediateObject=True, parentHidden=True, visibility=True) # The roots must be visible (the assemblies) for parent in top_parents: if not _is_visible(parent): cls.log.error("Invisible parent (root node) is not " "allowed: {0}".format(parent)) invalid.add(parent) # Ensure at least one shape is visible if not any(_is_visible(shape) for shape in shapes): cls.log.error("No visible shapes in the model instance") invalid.update(shapes) return list(invalid) def process(self, instance): if not self.is_active(instance.data): return invalid = self.get_invalid(instance) if invalid: raise PublishValidationError( title="Model content is invalid", message="See log for more details" ) ================================================ FILE: openpype/hosts/maya/plugins/publish/validate_model_name.py ================================================ # -*- coding: utf-8 -*- """Validate model nodes names.""" import os import platform import re import gridfs import pyblish.api from maya import cmds import openpype.hosts.maya.api.action from openpype.client.mongo import OpenPypeMongoConnection from openpype.hosts.maya.api.shader_definition_editor import ( DEFINITION_FILENAME) from openpype.pipeline import legacy_io from openpype.pipeline.publish import ( OptionalPyblishPluginMixin, PublishValidationError, ValidateContentsOrder) class ValidateModelName(pyblish.api.InstancePlugin, OptionalPyblishPluginMixin): """Validate name of model starts with (somename)_###_(materialID)_GEO materialID must be present in list padding number doesn't have limit """ optional = True order = ValidateContentsOrder hosts = ["maya"] families = ["model"] label = "Model Name" actions = [openpype.hosts.maya.api.action.SelectInvalidAction] material_file = None database_file = DEFINITION_FILENAME @classmethod def get_invalid(cls, instance): """Get invalid nodes.""" use_db = cls.database def is_group(group_name): """Find out if supplied transform is group or not.""" try: children = cmds.listRelatives(group_name, children=True) for child in children: if not cmds.ls(child, transforms=True): return False return True except Exception: return False invalid = [] content_instance = instance.data.get("setMembers", None) if not content_instance: cls.log.error("Instance has no nodes!") return True pass # validate top level group name assemblies = cmds.ls(content_instance, assemblies=True, long=True) if len(assemblies) != 1: cls.log.error("Must have exactly one top group") return assemblies or True top_group = assemblies[0] regex = cls.top_level_regex r = re.compile(regex) m = r.match(top_group) project_name = instance.context.data["projectName"] current_asset_name = instance.context.data["asset"] if m is None: cls.log.error("invalid name on: {}".format(top_group)) cls.log.error("name doesn't match regex {}".format(regex)) invalid.append(top_group) else: if "asset" in r.groupindex: if m.group("asset") != current_asset_name: cls.log.error("Invalid asset name in top level group.") return top_group if "subset" in r.groupindex: if m.group("subset") != instance.data.get("subset"): cls.log.error("Invalid subset name in top level group.") return top_group if "project" in r.groupindex: if m.group("project") != project_name: cls.log.error("Invalid project name in top level group.") return top_group descendants = cmds.listRelatives(content_instance, allDescendents=True, fullPath=True) or [] descendants = cmds.ls(descendants, noIntermediate=True, long=True) trns = cmds.ls(descendants, long=False, type='transform') # filter out groups filtered = [node for node in trns if not is_group(node)] # load shader list file as utf-8 shaders = [] if not use_db: material_file = cls.material_file[platform.system().lower()] if material_file: if os.path.isfile(material_file): shader_file = open(material_file, "r") shaders = shader_file.readlines() shader_file.close() else: cls.log.error("Missing shader name definition file.") return True else: client = OpenPypeMongoConnection.get_mongo_client() fs = gridfs.GridFS(client[os.getenv("OPENPYPE_DATABASE_NAME")]) shader_file = fs.find_one({"filename": cls.database_file}) if not shader_file: cls.log.error("Missing shader name definition in database.") return True shaders = shader_file.read().splitlines() shader_file.close() # strip line endings from list shaders = [s.rstrip() for s in shaders if s.rstrip()] # compile regex for testing names regex = cls.regex r = re.compile(regex) for obj in filtered: cls.log.debug("testing: {}".format(obj)) m = r.match(obj) if m is None: cls.log.error("invalid name on: {}".format(obj)) invalid.append(obj) else: # if we have shader files and shader named group is in # regex, test this group against names in shader file if "shader" in r.groupindex and shaders: try: if not m.group('shader') in shaders: cls.log.error( "invalid materialID on: {0} ({1})".format( obj, m.group('shader'))) invalid.append(obj) except IndexError: # shader named group doesn't match cls.log.error( "shader group doesn't match: {}".format(obj)) invalid.append(obj) return invalid def process(self, instance): """Plugin entry point.""" if not self.is_active(instance.data): return invalid = self.get_invalid(instance) if invalid: raise PublishValidationError( "Model naming is invalid. See the log.") ================================================ FILE: openpype/hosts/maya/plugins/publish/validate_mvlook_contents.py ================================================ import os import pyblish.api import openpype.hosts.maya.api.action from openpype.pipeline.publish import ( ValidateContentsOrder, OptionalPyblishPluginMixin, PublishValidationError ) COLOUR_SPACES = ['sRGB', 'linear', 'auto'] MIPMAP_EXTENSIONS = ['tdl'] class ValidateMvLookContents(pyblish.api.InstancePlugin, OptionalPyblishPluginMixin): order = ValidateContentsOrder families = ['mvLook'] hosts = ['maya'] label = 'Validate mvLook Data' actions = [openpype.hosts.maya.api.action.SelectInvalidAction] # Allow this validation step to be skipped when you just need to # get things pushed through. optional = True # These intents get enforced checks, other ones get warnings. enforced_intents = ['-', 'Final'] def process(self, instance): if not self.is_active(instance.data): return intent = instance.context.data['intent']['value'] publishMipMap = instance.data["publishMipMap"] enforced = True if intent in self.enforced_intents: self.log.debug("This validation will be enforced: '{}'" .format(intent)) else: enforced = False self.log.debug("This validation will NOT be enforced: '{}'" .format(intent)) if not instance[:]: raise PublishValidationError("Instance is empty") invalid = set() resources = instance.data.get("resources", []) for resource in resources: files = resource["files"] self.log.debug( "Resource '{}', files: [{}]".format(resource, files)) node = resource["node"] if len(files) == 0: self.log.error("File node '{}' uses no or non-existing " "files".format(node)) invalid.add(node) continue for fname in files: if not self.valid_file(fname): self.log.error("File node '{}'/'{}' is not valid" .format(node, fname)) invalid.add(node) if publishMipMap and not self.is_or_has_mipmap(fname, files): msg = "File node '{}'/'{}' does not have a mipmap".format( node, fname) if enforced: invalid.add(node) self.log.error(msg) raise PublishValidationError(msg) else: self.log.warning(msg) if invalid: raise PublishValidationError( "'{}' has invalid look content".format(instance.name) ) def valid_file(self, fname): self.log.debug("Checking validity of '{}'".format(fname)) if not os.path.exists(fname): return False if os.path.getsize(fname) == 0: return False return True def is_or_has_mipmap(self, fname, files): ext = os.path.splitext(fname)[1][1:] if ext in MIPMAP_EXTENSIONS: self.log.debug(" - Is a mipmap '{}'".format(fname)) return True for colour_space in COLOUR_SPACES: for mipmap_ext in MIPMAP_EXTENSIONS: mipmap_fname = '.'.join([fname, colour_space, mipmap_ext]) if mipmap_fname in files: self.log.debug( " - Has a mipmap '{}'".format(mipmap_fname)) return True return False ================================================ FILE: openpype/hosts/maya/plugins/publish/validate_no_animation.py ================================================ from maya import cmds import pyblish.api import openpype.hosts.maya.api.action from openpype.pipeline.publish import ( ValidateContentsOrder, OptionalPyblishPluginMixin, PublishValidationError ) def _as_report_list(values, prefix="- ", suffix="\n"): """Return list as bullet point list for a report""" if not values: return "" return prefix + (suffix + prefix).join(values) class ValidateNoAnimation(pyblish.api.Validator, OptionalPyblishPluginMixin): """Ensure no keyframes on nodes in the Instance. Even though a Model would extract without animCurves correctly this avoids getting different output from a model when extracted from a different frame than the first frame. (Might be overly restrictive though) """ order = ValidateContentsOrder label = "No Animation" hosts = ["maya"] families = ["model"] optional = True actions = [openpype.hosts.maya.api.action.SelectInvalidAction] def process(self, instance): if not self.is_active(instance.data): return invalid = self.get_invalid(instance) if invalid: raise PublishValidationError( "Keyframes found on:\n\n{0}".format( _as_report_list(sorted(invalid)) ), title="Keyframes on model" ) @staticmethod def get_invalid(instance): nodes = instance[:] if not nodes: return [] curves = cmds.keyframe(nodes, query=True, name=True) if curves: return list(set(cmds.listConnections(curves))) return [] ================================================ FILE: openpype/hosts/maya/plugins/publish/validate_no_default_camera.py ================================================ from maya import cmds import pyblish.api import openpype.hosts.maya.api.action from openpype.pipeline.publish import ( ValidateContentsOrder, PublishValidationError, OptionalPyblishPluginMixin ) def _as_report_list(values, prefix="- ", suffix="\n"): """Return list as bullet point list for a report""" if not values: return "" return prefix + (suffix + prefix).join(values) class ValidateNoDefaultCameras(pyblish.api.InstancePlugin, OptionalPyblishPluginMixin): """Ensure no default (startup) cameras are in the instance. This might be unnecessary. In the past there were some issues with referencing/importing files that contained the start up cameras overriding settings when being loaded and sometimes being skipped. """ order = ValidateContentsOrder hosts = ['maya'] families = ['camera'] label = "No Default Cameras" actions = [openpype.hosts.maya.api.action.SelectInvalidAction] optional = False @staticmethod def get_invalid(instance): cameras = cmds.ls(instance, type='camera', long=True) return [cam for cam in cameras if cmds.camera(cam, query=True, startupCamera=True)] def process(self, instance): """Process all the cameras in the instance""" if not self.is_active(instance.data): return invalid = self.get_invalid(instance) if invalid: raise PublishValidationError( "Default cameras found:\n\n{0}".format( _as_report_list(sorted(invalid)) ), title="Default cameras" ) ================================================ FILE: openpype/hosts/maya/plugins/publish/validate_no_namespace.py ================================================ import maya.cmds as cmds import pyblish.api from openpype.pipeline.publish import ( RepairAction, ValidateContentsOrder, PublishValidationError, OptionalPyblishPluginMixin ) import openpype.hosts.maya.api.action def _as_report_list(values, prefix="- ", suffix="\n"): """Return list as bullet point list for a report""" if not values: return "" return prefix + (suffix + prefix).join(values) def get_namespace(node_name): # ensure only node's name (not parent path) node_name = node_name.rsplit("|", 1)[-1] # ensure only namespace return node_name.rpartition(":")[0] class ValidateNoNamespace(pyblish.api.InstancePlugin, OptionalPyblishPluginMixin): """Ensure the nodes don't have a namespace""" order = ValidateContentsOrder hosts = ['maya'] families = ['model'] label = 'No Namespaces' actions = [openpype.hosts.maya.api.action.SelectInvalidAction, RepairAction] optional = False @staticmethod def get_invalid(instance): nodes = cmds.ls(instance, long=True) return [node for node in nodes if get_namespace(node)] def process(self, instance): """Process all the nodes in the instance""" if not self.is_active(instance.data): return invalid = self.get_invalid(instance) if invalid: raise PublishValidationError( "Namespaces found:\n\n{0}".format( _as_report_list(sorted(invalid)) ), title="Namespaces in model" ) @classmethod def repair(cls, instance): """Remove all namespaces from the nodes in the instance""" invalid = cls.get_invalid(instance) # Iterate over the nodes by long to short names to iterate the lowest # in hierarchy nodes first. This way we avoid having renamed parents # before renaming children nodes for node in sorted(invalid, key=len, reverse=True): node_name = node.rsplit("|", 1)[-1] node_name_without_namespace = node_name.rsplit(":")[-1] cmds.rename(node, node_name_without_namespace) ================================================ FILE: openpype/hosts/maya/plugins/publish/validate_no_null_transforms.py ================================================ import maya.cmds as cmds import pyblish.api import openpype.hosts.maya.api.action from openpype.pipeline.publish import ( RepairAction, ValidateContentsOrder, PublishValidationError, OptionalPyblishPluginMixin ) def _as_report_list(values, prefix="- ", suffix="\n"): """Return list as bullet point list for a report""" if not values: return "" return prefix + (suffix + prefix).join(values) def has_shape_children(node): # Check if any descendants allDescendents = cmds.listRelatives(node, allDescendents=True, fullPath=True) if not allDescendents: return False # Check if there are any shapes at all shapes = cmds.ls(allDescendents, shapes=True) if not shapes: return False # Check if all descendent shapes are intermediateObjects; # if so we consider this node a null node and return False. if all(cmds.getAttr('{0}.intermediateObject'.format(x)) for x in shapes): return False return True class ValidateNoNullTransforms(pyblish.api.InstancePlugin, OptionalPyblishPluginMixin): """Ensure no null transforms are in the scene. Warning: Transforms with only intermediate shapes are also considered null transforms. These transform nodes could potentially be used in your construction history, so take care when automatically fixing this or when deleting the empty transforms manually. """ order = ValidateContentsOrder hosts = ['maya'] families = ['model'] label = 'No Empty/Null Transforms' actions = [RepairAction, openpype.hosts.maya.api.action.SelectInvalidAction] optional = False @staticmethod def get_invalid(instance): """Return invalid transforms in instance""" transforms = cmds.ls(instance, type='transform', long=True) invalid = [] for transform in transforms: if not has_shape_children(transform): invalid.append(transform) return invalid def process(self, instance): """Process all the transform nodes in the instance """ if not self.is_active(instance.data): return invalid = self.get_invalid(instance) if invalid: raise PublishValidationError( "Empty transforms found without shapes:\n\n{0}".format( _as_report_list(sorted(invalid)) ), title="Empty transforms" ) @classmethod def repair(cls, instance): """Delete all null transforms. Note: If the node is used elsewhere (eg. connection to attributes or in history) deletion might mess up things. """ invalid = cls.get_invalid(instance) if invalid: cmds.delete(invalid) ================================================ FILE: openpype/hosts/maya/plugins/publish/validate_no_unknown_nodes.py ================================================ from maya import cmds import pyblish.api import openpype.hosts.maya.api.action from openpype.pipeline.publish import ( ValidateContentsOrder, OptionalPyblishPluginMixin, PublishValidationError ) def _as_report_list(values, prefix="- ", suffix="\n"): """Return list as bullet point list for a report""" if not values: return "" return prefix + (suffix + prefix).join(values) class ValidateNoUnknownNodes(pyblish.api.InstancePlugin, OptionalPyblishPluginMixin): """Checks to see if there are any unknown nodes in the instance. This often happens if nodes from plug-ins are used but are not available on this machine. Note: Some studios use unknown nodes to store data on (as attributes) because it's a lightweight node. """ order = ValidateContentsOrder hosts = ['maya'] families = ['model', 'rig'] optional = True label = "Unknown Nodes" actions = [openpype.hosts.maya.api.action.SelectInvalidAction] @staticmethod def get_invalid(instance): return cmds.ls(instance, type='unknown') def process(self, instance): """Process all the nodes in the instance""" if not self.is_active(instance.data): return invalid = self.get_invalid(instance) if invalid: raise PublishValidationError( "Unknown nodes found:\n\n{0}".format( _as_report_list(sorted(invalid)) ), title="Unknown nodes" ) ================================================ FILE: openpype/hosts/maya/plugins/publish/validate_no_vraymesh.py ================================================ import pyblish.api from maya import cmds from openpype.pipeline.publish import ( PublishValidationError, OptionalPyblishPluginMixin ) def _as_report_list(values, prefix="- ", suffix="\n"): """Return list as bullet point list for a report""" if not values: return "" return prefix + (suffix + prefix).join(values) class ValidateNoVRayMesh(pyblish.api.InstancePlugin, OptionalPyblishPluginMixin): """Validate there are no VRayMesh objects in the instance""" order = pyblish.api.ValidatorOrder label = 'No V-Ray Proxies (VRayMesh)' families = ["pointcache"] optional = False def process(self, instance): if not self.is_active(instance.data): return if not cmds.pluginInfo("vrayformaya", query=True, loaded=True): return shapes = cmds.ls(instance, shapes=True, type="mesh") inputs = cmds.listConnections(shapes, destination=False, source=True) or [] vray_meshes = cmds.ls(inputs, type='VRayMesh') if vray_meshes: raise PublishValidationError( "Meshes that are V-Ray Proxies should not be in an Alembic " "pointcache.\n" "Found V-Ray proxies:\n\n{}".format( _as_report_list(sorted(vray_meshes)) ), title="V-Ray Proxies in pointcache" ) ================================================ FILE: openpype/hosts/maya/plugins/publish/validate_node_ids.py ================================================ import pyblish.api from openpype.pipeline.publish import ( ValidatePipelineOrder, PublishXmlValidationError ) import openpype.hosts.maya.api.action from openpype.hosts.maya.api import lib class ValidateNodeIDs(pyblish.api.InstancePlugin): """Validate nodes have a Colorbleed Id. When IDs are missing from nodes *save your scene* and they should be automatically generated because IDs are created on non-referenced nodes in Maya upon scene save. """ order = ValidatePipelineOrder label = 'Instance Nodes Have ID' hosts = ['maya'] families = ["model", "look", "rig", "pointcache", "animation", "yetiRig", "assembly"] actions = [openpype.hosts.maya.api.action.SelectInvalidAction, openpype.hosts.maya.api.action.GenerateUUIDsOnInvalidAction] def process(self, instance): """Process all meshes""" # Ensure all nodes have a cbId invalid = self.get_invalid(instance) if invalid: names = "\n".join( "- {}".format(node) for node in invalid ) raise PublishXmlValidationError( plugin=self, message="Nodes found without IDs: {}".format(invalid), formatting_data={"nodes": names} ) @classmethod def get_invalid(cls, instance): """Return the member nodes that are invalid""" # We do want to check the referenced nodes as it might be # part of the end product. id_nodes = lib.get_id_required_nodes(referenced_nodes=True, nodes=instance[:]) invalid = [n for n in id_nodes if not lib.get_id(n)] return invalid ================================================ FILE: openpype/hosts/maya/plugins/publish/validate_node_ids_deformed_shapes.py ================================================ import pyblish.api from maya import cmds import openpype.hosts.maya.api.action from openpype.hosts.maya.api import lib from openpype.pipeline.publish import ( PublishValidationError, RepairAction, ValidateContentsOrder) class ValidateNodeIdsDeformedShape(pyblish.api.InstancePlugin): """Validate if deformed shapes have related IDs to the original shapes. When a deformer is applied in the scene on a referenced mesh that already had deformers then Maya will create a new shape node for the mesh that does not have the original id. This validator checks whether the ids are valid on all the shape nodes in the instance. """ order = ValidateContentsOrder families = ['look'] hosts = ['maya'] label = 'Deformed shape ids' actions = [ openpype.hosts.maya.api.action.SelectInvalidAction, RepairAction ] def process(self, instance): """Process all the nodes in the instance""" # Ensure all nodes have a cbId and a related ID to the original shapes # if a deformer has been created on the shape invalid = self.get_invalid(instance) if invalid: raise PublishValidationError( ("Shapes found that are considered 'Deformed'" "without object ids: {0}").format(invalid)) @classmethod def get_invalid(cls, instance): """Get all nodes which do not match the criteria""" shapes = cmds.ls(instance[:], dag=True, leaf=True, shapes=True, long=True, noIntermediate=True) invalid = [] for shape in shapes: history_id = lib.get_id_from_sibling(shape) if history_id: current_id = lib.get_id(shape) if current_id != history_id: invalid.append(shape) return invalid @classmethod def repair(cls, instance): for node in cls.get_invalid(instance): # Get the original id from history history_id = lib.get_id_from_sibling(node) if not history_id: cls.log.error("Could not find ID in history for '%s'", node) continue lib.set_id(node, history_id, overwrite=True) ================================================ FILE: openpype/hosts/maya/plugins/publish/validate_node_ids_in_database.py ================================================ import pyblish.api import openpype.hosts.maya.api.action from openpype.client import get_assets from openpype.hosts.maya.api import lib from openpype.pipeline import legacy_io from openpype.pipeline.publish import ( PublishValidationError, ValidatePipelineOrder) class ValidateNodeIdsInDatabase(pyblish.api.InstancePlugin): """Validate if the CB Id is related to an asset in the database All nodes with the `cbId` attribute will be validated to ensure that the loaded asset in the scene is related to the current project. Tip: If there is an asset which is being reused from a different project please ensure the asset is republished in the new project """ order = ValidatePipelineOrder label = 'Node Ids in Database' hosts = ['maya'] families = ["*"] actions = [openpype.hosts.maya.api.action.SelectInvalidAction, openpype.hosts.maya.api.action.GenerateUUIDsOnInvalidAction] def process(self, instance): invalid = self.get_invalid(instance) if invalid: raise PublishValidationError( ("Found asset IDs which are not related to " "current project in instance: `{}`").format(instance.name)) @classmethod def get_invalid(cls, instance): invalid = [] # Get all id required nodes id_required_nodes = lib.get_id_required_nodes(referenced_nodes=True, nodes=instance[:]) # check ids against database ids project_name = legacy_io.active_project() asset_docs = get_assets(project_name, fields=["_id"]) db_asset_ids = { str(asset_doc["_id"]) for asset_doc in asset_docs } # Get all asset IDs for node in id_required_nodes: cb_id = lib.get_id(node) # Ignore nodes without id, those are validated elsewhere if not cb_id: continue asset_id = cb_id.split(":", 1)[0] if asset_id not in db_asset_ids: cls.log.error("`%s` has unassociated asset ID" % node) invalid.append(node) return invalid ================================================ FILE: openpype/hosts/maya/plugins/publish/validate_node_ids_related.py ================================================ import pyblish.api import openpype.hosts.maya.api.action from openpype.hosts.maya.api import lib from openpype.pipeline.publish import ( OptionalPyblishPluginMixin, PublishValidationError, ValidatePipelineOrder) class ValidateNodeIDsRelated(pyblish.api.InstancePlugin, OptionalPyblishPluginMixin): """Validate nodes have a related Colorbleed Id to the instance.data[asset] """ order = ValidatePipelineOrder label = 'Node Ids Related (ID)' hosts = ['maya'] families = ["model", "look", "rig"] optional = True actions = [openpype.hosts.maya.api.action.SelectInvalidAction, openpype.hosts.maya.api.action.GenerateUUIDsOnInvalidAction] def process(self, instance): """Process all nodes in instance (including hierarchy)""" if not self.is_active(instance.data): return # Ensure all nodes have a cbId invalid = self.get_invalid(instance) if invalid: raise PublishValidationError( ("Nodes IDs found that are not related to asset " "'{}' : {}").format(instance.data['asset'], invalid)) @classmethod def get_invalid(cls, instance): """Return the member nodes that are invalid""" invalid = list() asset_id = str(instance.data['assetEntity']["_id"]) # We do want to check the referenced nodes as we it might be # part of the end product for node in instance: _id = lib.get_id(node) if not _id: continue node_asset_id = _id.split(":", 1)[0] if node_asset_id != asset_id: invalid.append(node) return invalid ================================================ FILE: openpype/hosts/maya/plugins/publish/validate_node_ids_unique.py ================================================ from collections import defaultdict import pyblish.api from openpype.pipeline.publish import ( ValidatePipelineOrder, PublishValidationError ) import openpype.hosts.maya.api.action from openpype.hosts.maya.api import lib class ValidateNodeIdsUnique(pyblish.api.InstancePlugin): """Validate the nodes in the instance have a unique Colorbleed Id Here we ensure that what has been added to the instance is unique """ order = ValidatePipelineOrder label = 'Non Duplicate Instance Members (ID)' hosts = ['maya'] families = ["model", "look", "rig", "yetiRig"] actions = [openpype.hosts.maya.api.action.SelectInvalidAction, openpype.hosts.maya.api.action.GenerateUUIDsOnInvalidAction] def process(self, instance): """Process all meshes""" # Ensure all nodes have a cbId invalid = self.get_invalid(instance) if invalid: label = "Nodes found with non-unique asset IDs" raise PublishValidationError( message="{}: {}".format(label, invalid), title="Non-unique asset ids on nodes", description="{}\n- {}".format(label, "\n- ".join(sorted(invalid))) ) @classmethod def get_invalid(cls, instance): """Return the member nodes that are invalid""" # Check only non intermediate shapes # todo: must the instance itself ensure to have no intermediates? # todo: how come there are intermediates? from maya import cmds instance_members = cmds.ls(instance, noIntermediate=True, long=True) # Collect each id with their members ids = defaultdict(list) for member in instance_members: object_id = lib.get_id(member) if not object_id: continue ids[object_id].append(member) # Take only the ids with more than one member invalid = list() _iteritems = getattr(ids, "iteritems", ids.items) for _ids, members in _iteritems(): if len(members) > 1: cls.log.error("ID found on multiple nodes: '%s'" % members) invalid.extend(members) return invalid ================================================ FILE: openpype/hosts/maya/plugins/publish/validate_node_no_ghosting.py ================================================ from maya import cmds import pyblish.api import openpype.hosts.maya.api.action from openpype.pipeline.publish import ( ValidateContentsOrder, OptionalPyblishPluginMixin ) class ValidateNodeNoGhosting(pyblish.api.InstancePlugin, OptionalPyblishPluginMixin): """Ensure nodes do not have ghosting enabled. If one would publish towards a non-Maya format it's likely that stats like ghosting won't be exported, eg. exporting to Alembic. Instead of creating many micro-managing checks (like this one) to ensure attributes have not been changed from their default it could be more efficient to export to a format that will never hold such data anyway. """ order = ValidateContentsOrder hosts = ['maya'] families = ['model', 'rig'] label = "No Ghosting" actions = [openpype.hosts.maya.api.action.SelectInvalidAction] optional = False _attributes = {'ghosting': 0} @classmethod def get_invalid(cls, instance): # Transforms and shapes seem to have ghosting nodes = cmds.ls(instance, long=True, type=['transform', 'shape']) invalid = [] for node in nodes: _iteritems = getattr( cls._attributes, "iteritems", cls._attributes.items ) for attr, required_value in _iteritems(): if cmds.attributeQuery(attr, node=node, exists=True): value = cmds.getAttr('{0}.{1}'.format(node, attr)) if value != required_value: invalid.append(node) return invalid def process(self, instance): if not self.is_active(instance.data): return invalid = self.get_invalid(instance) if invalid: raise ValueError("Nodes with ghosting enabled found: " "{0}".format(invalid)) ================================================ FILE: openpype/hosts/maya/plugins/publish/validate_plugin_path_attributes.py ================================================ import os from maya import cmds import pyblish.api from openpype.hosts.maya.api.lib import pairwise from openpype.hosts.maya.api.action import SelectInvalidAction from openpype.pipeline.publish import ( ValidateContentsOrder, PublishValidationError, OptionalPyblishPluginMixin ) class ValidatePluginPathAttributes(pyblish.api.InstancePlugin, OptionalPyblishPluginMixin): """ Validate plug-in path attributes point to existing file paths. """ order = ValidateContentsOrder hosts = ['maya'] families = ["workfile"] label = "Plug-in Path Attributes" actions = [SelectInvalidAction] optional = False # Attributes are defined in project settings attribute = [] @classmethod def get_invalid(cls, instance): invalid = list() file_attrs = cls.attribute if not file_attrs: return invalid # Consider only valid node types to avoid "Unknown object type" warning all_node_types = set(cmds.allNodeTypes()) node_types = [ key for key in file_attrs.keys() if key in all_node_types ] for node, node_type in pairwise(cmds.ls(type=node_types, showType=True)): # get the filepath file_attr = "{}.{}".format(node, file_attrs[node_type]) filepath = cmds.getAttr(file_attr) if filepath and not os.path.exists(filepath): cls.log.error("{} '{}' uses non-existing filepath: {}" .format(node_type, node, filepath)) invalid.append(node) return invalid def process(self, instance): """Process all directories Set as Filenames in Non-Maya Nodes""" if not self.is_active(instance.data): return invalid = self.get_invalid(instance) if invalid: raise PublishValidationError( title="Plug-in Path Attributes", message="Non-existent filepath found on nodes: {}".format( ", ".join(invalid) ), description=( "## Plug-in nodes use invalid filepaths\n" "The workfile contains nodes from plug-ins that use " "filepaths which do not exist.\n\n" "Please make sure their filepaths are correct and the " "files exist on disk." ) ) ================================================ FILE: openpype/hosts/maya/plugins/publish/validate_render_image_rule.py ================================================ import os import pyblish.api from maya import cmds from openpype.pipeline.publish import ( PublishValidationError, RepairAction, ValidateContentsOrder, OptionalPyblishPluginMixin ) class ValidateRenderImageRule(pyblish.api.InstancePlugin, OptionalPyblishPluginMixin): """Validates Maya Workpace "images" file rule matches project settings. This validates against the configured default render image folder: Studio Settings > Project > Maya > Render Settings > Default render image folder. """ order = ValidateContentsOrder label = "Images File Rule (Workspace)" hosts = ["maya"] families = ["renderlayer"] actions = [RepairAction] optional = False def process(self, instance): if not self.is_active(instance.data): return required_images_rule = os.path.normpath( self.get_default_render_image_folder(instance) ) current_images_rule = os.path.normpath( cmds.workspace(fileRuleEntry="images") ) if current_images_rule != required_images_rule: raise PublishValidationError( ( "Invalid workspace `images` file rule value: '{}'. " "Must be set to: '{}'" ).format(current_images_rule, required_images_rule)) @classmethod def repair(cls, instance): required_images_rule = cls.get_default_render_image_folder(instance) current_images_rule = cmds.workspace(fileRuleEntry="images") if current_images_rule != required_images_rule: cmds.workspace(fileRule=("images", required_images_rule)) cmds.workspace(saveWorkspace=True) @classmethod def get_default_render_image_folder(cls, instance): staging_dir = instance.data.get("stagingDir") if staging_dir: cls.log.debug( "Staging dir found: \"{}\". Ignoring setting from " "`project_settings/maya/RenderSettings/" "default_render_image_folder`.".format(staging_dir) ) return staging_dir return instance.context.data.get('project_settings')\ .get('maya') \ .get('RenderSettings') \ .get('default_render_image_folder') ================================================ FILE: openpype/hosts/maya/plugins/publish/validate_render_no_default_cameras.py ================================================ from maya import cmds import pyblish.api import openpype.hosts.maya.api.action from openpype.pipeline.publish import ( ValidateContentsOrder, PublishValidationError, OptionalPyblishPluginMixin ) class ValidateRenderNoDefaultCameras(pyblish.api.InstancePlugin, OptionalPyblishPluginMixin): """Ensure no default (startup) cameras are to be rendered.""" order = ValidateContentsOrder hosts = ['maya'] families = ['renderlayer'] label = "No Default Cameras Renderable" actions = [openpype.hosts.maya.api.action.SelectInvalidAction] optional = False @staticmethod def get_invalid(instance): renderable = set(instance.data["cameras"]) # Collect default cameras cameras = cmds.ls(type='camera', long=True) defaults = set(cam for cam in cameras if cmds.camera(cam, query=True, startupCamera=True)) return [cam for cam in renderable if cam in defaults] def process(self, instance): """Process all the cameras in the instance""" if not self.is_active(instance.data): return invalid = self.get_invalid(instance) if invalid: raise PublishValidationError( title="Rendering default cameras", message="Renderable default cameras " "found: {0}".format(invalid)) ================================================ FILE: openpype/hosts/maya/plugins/publish/validate_render_single_camera.py ================================================ import re import pyblish.api from maya import cmds import openpype.hosts.maya.api.action from openpype.hosts.maya.api.lib_rendersettings import RenderSettings from openpype.pipeline.publish import ( ValidateContentsOrder, PublishValidationError, OptionalPyblishPluginMixin ) class ValidateRenderSingleCamera(pyblish.api.InstancePlugin, OptionalPyblishPluginMixin): """Validate renderable camera count for layer andtoken. Pipeline is supporting multiple renderable cameras per layer, but image prefix must contain token. """ order = ValidateContentsOrder label = "Render Single Camera" hosts = ['maya'] families = ["renderlayer", "vrayscene"] actions = [openpype.hosts.maya.api.action.SelectInvalidAction] optional = False R_CAMERA_TOKEN = re.compile(r'%c| ', re.IGNORECASE) def process(self, instance): """Process all the cameras in the instance""" if not self.is_active(instance.data): return invalid = self.get_invalid(instance) if invalid: raise PublishValidationError("Invalid cameras for render.") @classmethod def get_invalid(cls, instance): cameras = instance.data.get("cameras", []) renderer = cmds.getAttr('defaultRenderGlobals.currentRenderer').lower() # handle various renderman names if renderer.startswith('renderman'): renderer = 'renderman' file_prefix = cmds.getAttr( RenderSettings.get_image_prefix_attr(renderer) ) if len(cameras) > 1: if re.search(cls.R_CAMERA_TOKEN, file_prefix): # if there is token in prefix and we have more then # 1 camera, all is ok. return cls.log.error("Multiple renderable cameras found for %s: %s " % (instance.data["setMembers"], cameras)) return [instance.data["setMembers"]] + cameras elif len(cameras) < 1: cls.log.error("No renderable cameras found for %s " % instance.data["setMembers"]) return [instance.data["setMembers"]] ================================================ FILE: openpype/hosts/maya/plugins/publish/validate_renderlayer_aovs.py ================================================ import pyblish.api import openpype.hosts.maya.api.action from openpype.client import get_subset_by_name from openpype.pipeline import legacy_io from openpype.pipeline.publish import ( PublishValidationError, OptionalPyblishPluginMixin ) class ValidateRenderLayerAOVs(pyblish.api.InstancePlugin, OptionalPyblishPluginMixin): """Validate created AOVs / RenderElement is registered in the database Each render element is registered as a product which is formatted based on the render layer and the render element, example: . This translates to something like this: CHAR.diffuse This check is needed to ensure the render output is still complete """ order = pyblish.api.ValidatorOrder + 0.1 label = "Render Passes / AOVs Are Registered" hosts = ["maya"] families = ["renderlayer"] actions = [openpype.hosts.maya.api.action.SelectInvalidAction] optional = False def process(self, instance): if not self.is_active(instance.data): return invalid = self.get_invalid(instance) if invalid: raise PublishValidationError( "Found unregistered subsets: {}".format(invalid)) def get_invalid(self, instance): invalid = [] project_name = legacy_io.active_project() asset_doc = instance.data["assetEntity"] render_passes = instance.data.get("renderPasses", []) for render_pass in render_passes: is_valid = self.validate_subset_registered( project_name, asset_doc, render_pass ) if not is_valid: invalid.append(render_pass) return invalid def validate_subset_registered(self, project_name, asset_doc, subset_name): """Check if subset is registered in the database under the asset""" return get_subset_by_name( project_name, subset_name, asset_doc["_id"], fields=["_id"] ) ================================================ FILE: openpype/hosts/maya/plugins/publish/validate_rendersettings.py ================================================ # -*- coding: utf-8 -*- """Maya validator for render settings.""" import re from collections import OrderedDict from maya import cmds, mel import pyblish.api from openpype.pipeline.publish import ( RepairAction, ValidateContentsOrder, PublishValidationError, ) from openpype.hosts.maya.api import lib from openpype.hosts.maya.api.lib_rendersettings import RenderSettings def convert_to_int_or_float(string_value): # Order of types are important here since float can convert string # representation of integer. types = [int, float] for t in types: try: result = t(string_value) except ValueError: continue else: return result # Neither integer or float. return string_value def get_redshift_image_format_labels(): """Return nice labels for Redshift image formats.""" var = "$g_redshiftImageFormatLabels" return mel.eval("{0}={0}".format(var)) class ValidateRenderSettings(pyblish.api.InstancePlugin): """Validates the global render settings * File Name Prefix must start with: ` ` all other token are customizable but sane values for Arnold are: ` / / _ ` token is supported also, useful for multiple renderable cameras per render layer. For Redshift omit token. Redshift will append it automatically if AOVs are enabled and if you user Multipart EXR it doesn't make much sense. * Frame Padding must be: * default: 4 * Animation must be toggle on, in Render Settings - Common tab: * vray: Animation on standard of specific * arnold: Frame / Animation ext: Any choice without "(Single Frame)" * redshift: Animation toggled on NOTE: The repair function of this plugin does not repair the animation setting of the render settings due to multiple possibilities. """ order = ValidateContentsOrder label = "Render Settings" hosts = ["maya"] families = ["renderlayer"] actions = [RepairAction] ImagePrefixes = { 'mentalray': 'defaultRenderGlobals.imageFilePrefix', 'vray': 'vraySettings.fileNamePrefix', 'arnold': 'defaultRenderGlobals.imageFilePrefix', 'renderman': 'rmanGlobals.imageFileFormat', 'redshift': 'defaultRenderGlobals.imageFilePrefix', 'mayahardware2': 'defaultRenderGlobals.imageFilePrefix', } ImagePrefixTokens = { 'mentalray': ' / / {aov_separator} ', # noqa: E501 'arnold': ' / / {aov_separator} ', # noqa: E501 'redshift': ' / / ', 'vray': ' / / ', 'renderman': ' {aov_separator} . . ', 'mayahardware2': ' / / ', } _aov_chars = { "dot": ".", "dash": "-", "underscore": "_" } redshift_AOV_prefix = " / {aov_separator} " # noqa: E501 renderman_dir_prefix = " / " R_AOV_TOKEN = re.compile( r'%a| | ', re.IGNORECASE) R_LAYER_TOKEN = re.compile( r'%l| | ', re.IGNORECASE) R_CAMERA_TOKEN = re.compile(r'%c|Camera>') R_SCENE_TOKEN = re.compile(r'%s| ', re.IGNORECASE) DEFAULT_PADDING = 4 VRAY_PREFIX = " / / " DEFAULT_PREFIX = " / / _ " def process(self, instance): invalid = self.get_invalid(instance) if invalid: raise PublishValidationError( title="Invalid Render Settings", message=("Invalid render settings found " "for '{}'!".format(instance.name)) ) @classmethod def get_invalid(cls, instance): invalid = False renderer = instance.data['renderer'] layer = instance.data['renderlayer'] cameras = instance.data.get("cameras", []) # Prefix attribute can return None when a value was never set prefix = lib.get_attr_in_layer(cls.ImagePrefixes[renderer], layer=layer) or "" padding = lib.get_attr_in_layer( attr=RenderSettings.get_padding_attr(renderer), layer=layer ) anim_override = lib.get_attr_in_layer("defaultRenderGlobals.animation", layer=layer) prefix = prefix.replace( "{aov_separator}", instance.data.get("aovSeparator", "_")) default_prefix = cls.ImagePrefixTokens[renderer] if not anim_override: invalid = True cls.log.error("Animation needs to be enabled. Use the same " "frame for start and end to render single frame") if not re.search(cls.R_LAYER_TOKEN, prefix): invalid = True cls.log.error("Wrong image prefix [ {} ] - " "doesn't have: ' ' or " "' ' token".format(prefix)) if len(cameras) > 1 and not re.search(cls.R_CAMERA_TOKEN, prefix): invalid = True cls.log.error("Wrong image prefix [ {} ] - " "doesn't have: ' ' token".format(prefix)) cls.log.error( "Note that to needs to have capital 'C' at the beginning") # renderer specific checks if renderer == "vray": vray_settings = cmds.ls(type="VRaySettingsNode") if not vray_settings: node = cmds.createNode("VRaySettingsNode") else: node = vray_settings[0] scene_sep = cmds.getAttr( "{}.fileNameRenderElementSeparator".format(node)) if scene_sep != instance.data.get("aovSeparator", "_"): cls.log.error("AOV separator is not set correctly.") invalid = True if renderer == "redshift": redshift_AOV_prefix = cls.redshift_AOV_prefix.replace( "{aov_separator}", instance.data.get("aovSeparator", "_") ) if re.search(cls.R_AOV_TOKEN, prefix): invalid = True cls.log.error(("Do not use AOV token [ {} ] - " "Redshift is using image prefixes per AOV so " "it doesn't make much sense using it in global" "image prefix").format(prefix)) # get redshift AOVs rs_aovs = cmds.ls(type="RedshiftAOV", referencedNodes=False) for aov in rs_aovs: aov_prefix = cmds.getAttr("{}.filePrefix".format(aov)) # check their image prefix if aov_prefix != redshift_AOV_prefix: cls.log.error(("AOV ({}) image prefix is not set " "correctly {} != {}").format( cmds.getAttr("{}.name".format(aov)), aov_prefix, redshift_AOV_prefix )) invalid = True # check aov file format aov_ext = cmds.getAttr("{}.fileFormat".format(aov)) default_ext = cmds.getAttr("redshiftOptions.imageFormat") aov_type = cmds.getAttr("{}.aovType".format(aov)) if aov_type == "Cryptomatte": # redshift Cryptomatte AOV always uses "Cryptomatte (EXR)" # so we ignore validating file format for it. pass elif default_ext != aov_ext: labels = get_redshift_image_format_labels() cls.log.error( "AOV file format {} does not match global file format " "{}".format(labels[aov_ext], labels[default_ext]) ) invalid = True if renderer == "renderman": file_prefix = cmds.getAttr("rmanGlobals.imageFileFormat") dir_prefix = cmds.getAttr("rmanGlobals.imageOutputDir") if file_prefix.lower() != prefix.lower(): invalid = True cls.log.error("Wrong image prefix [ {} ]".format(file_prefix)) if dir_prefix.lower() != cls.renderman_dir_prefix.lower(): invalid = True cls.log.error("Wrong directory prefix [ {} ]".format( dir_prefix)) if renderer == "arnold": multipart = cmds.getAttr("defaultArnoldDriver.mergeAOVs") if multipart: if re.search(cls.R_AOV_TOKEN, prefix): invalid = True cls.log.error("Wrong image prefix [ {} ] - " "You can't use ' ' token " "with merge AOVs turned on".format(prefix)) default_prefix = re.sub( cls.R_AOV_TOKEN, "", default_prefix) # remove aov token from prefix to pass validation default_prefix = default_prefix.split("{aov_separator}")[0] elif not re.search(cls.R_AOV_TOKEN, prefix): invalid = True cls.log.error("Wrong image prefix [ {} ] - " "doesn't have: ' ' or " "token".format(prefix)) default_prefix = default_prefix.replace( "{aov_separator}", instance.data.get("aovSeparator", "_")) if prefix.lower() != default_prefix.lower(): cls.log.warning("warning: prefix differs from " "recommended {}".format( default_prefix)) if padding != cls.DEFAULT_PADDING: invalid = True cls.log.error("Expecting padding of {} ( {} )".format( cls.DEFAULT_PADDING, "0" * cls.DEFAULT_PADDING)) # load validation definitions from settings settings_lights_flag = instance.context.data["project_settings"].get( "maya", {}).get( "RenderSettings", {}).get( "enable_all_lights", False) instance_lights_flag = instance.data.get("renderSetupIncludeLights") if settings_lights_flag != instance_lights_flag: cls.log.warning( "Instance flag for \"Render Setup Include Lights\" is set to " "{} and Settings flag is set to {}".format( instance_lights_flag, settings_lights_flag ) ) # go through definitions and test if such node.attribute exists. # if so, compare its value from the one required. for data in cls.get_nodes(instance, renderer): for node in data["nodes"]: try: render_value = cmds.getAttr( "{}.{}".format(node, data["attribute"]) ) except PublishValidationError: invalid = True cls.log.error( "Cannot get value of {}.{}".format( node, data["attribute"] ) ) else: if render_value not in data["values"]: invalid = True cls.log.error( "Invalid value {} set on {}.{}. Expecting " "{}".format( render_value, node, data["attribute"], data["values"] ) ) return invalid @classmethod def get_nodes(cls, instance, renderer): maya_settings = instance.context.data["project_settings"]["maya"] validation_settings = ( maya_settings["publish"]["ValidateRenderSettings"].get( "{}_render_attributes".format(renderer) ) or [] ) result = [] for attr, values in OrderedDict(validation_settings).items(): values = [convert_to_int_or_float(v) for v in values if v] # Validate the settings has values. if not values: cls.log.error( "Settings for {} is missing values.".format(attr) ) continue cls.log.debug("{}: {}".format(attr, values)) if "." not in attr: cls.log.warning( "Skipping invalid attribute defined in validation " "settings: \"{}\"".format(attr) ) continue node_type, attribute_name = attr.split(".", 1) # first get node of that type nodes = cmds.ls(type=node_type) if not nodes: cls.log.warning( "No nodes of type \"{}\" found.".format(node_type) ) continue result.append( { "attribute": attribute_name, "nodes": nodes, "values": values } ) return result @classmethod def repair(cls, instance): renderer = instance.data['renderer'] layer_node = instance.data['setMembers'] redshift_AOV_prefix = cls.redshift_AOV_prefix.replace( "{aov_separator}", instance.data.get("aovSeparator", "_") ) default_prefix = cls.ImagePrefixTokens[renderer].replace( "{aov_separator}", instance.data.get("aovSeparator", "_") ) for data in cls.get_nodes(instance, renderer): if not data["values"]: continue for node in data["nodes"]: lib.set_attribute(data["attribute"], data["values"][0], node) with lib.renderlayer(layer_node): # Repair animation must be enabled cmds.setAttr("defaultRenderGlobals.animation", True) # Repair prefix if renderer == "arnold": multipart = cmds.getAttr("defaultArnoldDriver.mergeAOVs") if multipart: separator_variations = [ "_ ", " _", " ", ] for variant in separator_variations: default_prefix = default_prefix.replace(variant, "") if renderer != "renderman": prefix_attr = RenderSettings.get_image_prefix_attr(renderer) fname_prefix = default_prefix cmds.setAttr(prefix_attr, fname_prefix, type="string") # Repair padding padding_attr = RenderSettings.get_padding_attr(renderer) cmds.setAttr(padding_attr, cls.DEFAULT_PADDING) else: # renderman handles stuff differently cmds.setAttr("rmanGlobals.imageFileFormat", default_prefix, type="string") cmds.setAttr("rmanGlobals.imageOutputDir", cls.renderman_dir_prefix, type="string") if renderer == "vray": vray_settings = cmds.ls(type="VRaySettingsNode") if not vray_settings: node = cmds.createNode("VRaySettingsNode") else: node = vray_settings[0] cmds.optionMenuGrp("vrayRenderElementSeparator", v=instance.data.get("aovSeparator", "_")) cmds.setAttr( "{}.fileNameRenderElementSeparator".format(node), instance.data.get("aovSeparator", "_"), type="string" ) if renderer == "redshift": # get redshift AOVs rs_aovs = cmds.ls(type="RedshiftAOV", referencedNodes=False) for aov in rs_aovs: # fix AOV prefixes cmds.setAttr( "{}.filePrefix".format(aov), redshift_AOV_prefix, type="string") # fix AOV file format default_ext = cmds.getAttr( "redshiftOptions.imageFormat", asString=True) cmds.setAttr( "{}.fileFormat".format(aov), default_ext) ================================================ FILE: openpype/hosts/maya/plugins/publish/validate_resolution.py ================================================ import pyblish.api from openpype.pipeline import ( PublishValidationError, OptionalPyblishPluginMixin ) from maya import cmds from openpype.pipeline.publish import RepairAction from openpype.hosts.maya.api import lib from openpype.hosts.maya.api.lib import reset_scene_resolution class ValidateResolution(pyblish.api.InstancePlugin, OptionalPyblishPluginMixin): """Validate the render resolution setting aligned with DB""" order = pyblish.api.ValidatorOrder families = ["renderlayer"] hosts = ["maya"] label = "Validate Resolution" actions = [RepairAction] optional = True def process(self, instance): if not self.is_active(instance.data): return invalid = self.get_invalid_resolution(instance) if invalid: raise PublishValidationError( "Render resolution is invalid. See log for details.", description=( "Wrong render resolution setting. " "Please use repair button to fix it.\n\n" "If current renderer is V-Ray, " "make sure vraySettings node has been created." ) ) @classmethod def get_invalid_resolution(cls, instance): width, height, pixelAspect = cls.get_db_resolution(instance) current_renderer = instance.data["renderer"] layer = instance.data["renderlayer"] invalid = False if current_renderer == "vray": vray_node = "vraySettings" if cmds.objExists(vray_node): current_width = lib.get_attr_in_layer( "{}.width".format(vray_node), layer=layer) current_height = lib.get_attr_in_layer( "{}.height".format(vray_node), layer=layer) current_pixelAspect = lib.get_attr_in_layer( "{}.pixelAspect".format(vray_node), layer=layer ) else: cls.log.error( "Can't detect VRay resolution because there is no node " "named: `{}`".format(vray_node) ) return True else: current_width = lib.get_attr_in_layer( "defaultResolution.width", layer=layer) current_height = lib.get_attr_in_layer( "defaultResolution.height", layer=layer) current_pixelAspect = lib.get_attr_in_layer( "defaultResolution.pixelAspect", layer=layer ) if current_width != width or current_height != height: cls.log.error( "Render resolution {}x{} does not match " "asset resolution {}x{}".format( current_width, current_height, width, height )) invalid = True if current_pixelAspect != pixelAspect: cls.log.error( "Render pixel aspect {} does not match " "asset pixel aspect {}".format( current_pixelAspect, pixelAspect )) invalid = True return invalid @classmethod def get_db_resolution(cls, instance): asset_doc = instance.data["assetEntity"] project_doc = instance.context.data["projectEntity"] for data in [asset_doc["data"], project_doc["data"]]: if ( "resolutionWidth" in data and "resolutionHeight" in data and "pixelAspect" in data ): width = data["resolutionWidth"] height = data["resolutionHeight"] pixelAspect = data["pixelAspect"] return int(width), int(height), float(pixelAspect) # Defaults if not found in asset document or project document return 1920, 1080, 1.0 @classmethod def repair(cls, instance): # Usually without renderlayer overrides the renderlayers # all share the same resolution value - so fixing the first # will have fixed all the others too. It's much faster to # check whether it's invalid first instead of switching # into all layers individually if not cls.get_invalid_resolution(instance): cls.log.debug( "Nothing to repair on instance: {}".format(instance) ) return layer_node = instance.data['setMembers'] with lib.renderlayer(layer_node): reset_scene_resolution() ================================================ FILE: openpype/hosts/maya/plugins/publish/validate_resources.py ================================================ import os from collections import defaultdict import pyblish.api from openpype.pipeline.publish import ( ValidateContentsOrder, PublishValidationError ) class ValidateResources(pyblish.api.InstancePlugin): """Validates mapped resources. These are external files to the current application, for example these could be textures, image planes, cache files or other linked media. This validates: - The resources have unique filenames (without extension) """ order = ValidateContentsOrder label = "Resources Unique" def process(self, instance): resources = instance.data.get("resources", []) if not resources: self.log.debug("No resources to validate..") return basenames = defaultdict(set) for resource in resources: files = resource.get("files", []) for filename in files: # Use normalized paths in comparison and ignore case # sensitivity filename = os.path.normpath(filename).lower() basename = os.path.splitext(os.path.basename(filename))[0] basenames[basename].add(filename) invalid_resources = list() for basename, sources in basenames.items(): if len(sources) > 1: invalid_resources.extend(sources) self.log.error( "Non-unique resource name: {0}" "{0} (sources: {1})".format( basename, list(sources) ) ) if invalid_resources: raise PublishValidationError("Invalid resources in instance.") ================================================ FILE: openpype/hosts/maya/plugins/publish/validate_review.py ================================================ import pyblish.api from openpype.pipeline.publish import ( ValidateContentsOrder, PublishValidationError ) class ValidateReview(pyblish.api.InstancePlugin): """Validate review.""" order = ValidateContentsOrder label = "Validate Review" families = ["review"] def process(self, instance): cameras = instance.data["cameras"] # validate required settings if len(cameras) == 0: raise PublishValidationError( "No camera found in review instance: {}".format(instance) ) elif len(cameras) > 2: raise PublishValidationError( "Only a single camera is allowed for a review instance but " "more than one camera found in review instance: {}. " "Cameras found: {}".format(instance, ", ".join(cameras)) ) self.log.debug('camera: {}'.format(instance.data["review_camera"])) ================================================ FILE: openpype/hosts/maya/plugins/publish/validate_rig_contents.py ================================================ import pyblish.api from maya import cmds import openpype.hosts.maya.api.action from openpype.pipeline.publish import ( PublishValidationError, ValidateContentsOrder, OptionalPyblishPluginMixin ) class ValidateRigContents(pyblish.api.InstancePlugin, OptionalPyblishPluginMixin): """Ensure rig contains pipeline-critical content Every rig must contain at least two object sets: "controls_SET" - Set of all animatable controls "out_SET" - Set of all cacheable meshes """ order = ValidateContentsOrder label = "Rig Contents" hosts = ["maya"] families = ["rig"] action = [openpype.hosts.maya.api.action.SelectInvalidAction] optional = True accepted_output = ["mesh", "transform"] accepted_controllers = ["transform"] def process(self, instance): if not self.is_active(instance.data): return invalid = self.get_invalid(instance) if invalid: raise PublishValidationError( "Invalid rig content. See log for details.") @classmethod def get_invalid(cls, instance): # Find required sets by suffix required, rig_sets = cls.get_nodes(instance) cls.validate_missing_objectsets(instance, required, rig_sets) controls_set = rig_sets["controls_SET"] out_set = rig_sets["out_SET"] # Ensure contents in sets and retrieve long path for all objects output_content = cmds.sets(out_set, query=True) or [] if not output_content: raise PublishValidationError("Must have members in rig out_SET") output_content = cmds.ls(output_content, long=True) controls_content = cmds.sets(controls_set, query=True) or [] if not controls_content: raise PublishValidationError( "Must have members in rig controls_SET" ) controls_content = cmds.ls(controls_content, long=True) rig_content = output_content + controls_content invalid_hierarchy = cls.invalid_hierarchy(instance, rig_content) # Additional validations invalid_geometry = cls.validate_geometry(output_content) invalid_controls = cls.validate_controls(controls_content) error = False if invalid_hierarchy: cls.log.error("Found nodes which reside outside of root group " "while they are set up for publishing." "\n%s" % invalid_hierarchy) error = True if invalid_controls: cls.log.error("Only transforms can be part of the controls_SET." "\n%s" % invalid_controls) error = True if invalid_geometry: cls.log.error("Only meshes can be part of the out_SET\n%s" % invalid_geometry) error = True if error: return invalid_hierarchy + invalid_controls + invalid_geometry @classmethod def validate_missing_objectsets(cls, instance, required_objsets, rig_sets): """Validate missing objectsets in rig sets Args: instance (str): instance required_objsets (list): list of objectset names rig_sets (list): list of rig sets Raises: PublishValidationError: When the error is raised, it will show which instance has the missing object sets """ missing = [ key for key in required_objsets if key not in rig_sets ] if missing: raise PublishValidationError( "%s is missing sets: %s" % (instance, ", ".join(missing)) ) @classmethod def invalid_hierarchy(cls, instance, content): """ Check if all rig set members are within the hierarchy of the rig root Args: instance (str): instance content (list): list of content from rig sets Raises: PublishValidationError: It means no dag nodes in the rig instance Returns: list: invalid hierarchy """ # Ensure there are at least some transforms or dag nodes # in the rig instance set_members = instance.data['setMembers'] if not cmds.ls(set_members, type="dagNode", long=True): raise PublishValidationError( "No dag nodes in the rig instance. " "(Empty instance?)" ) # Validate members are inside the hierarchy from root node root_nodes = cmds.ls(set_members, assemblies=True, long=True) hierarchy = cmds.listRelatives(root_nodes, allDescendents=True, fullPath=True) + root_nodes hierarchy = set(hierarchy) invalid_hierarchy = [] for node in content: if node not in hierarchy: invalid_hierarchy.append(node) return invalid_hierarchy @classmethod def validate_geometry(cls, set_members): """ Checks if the node types of the set members valid Args: set_members: list of nodes of the controls_set hierarchy: list of nodes which reside under the root node Returns: errors (list) """ # Validate all shape types invalid = [] shapes = cmds.listRelatives(set_members, allDescendents=True, shapes=True, fullPath=True) or [] all_shapes = cmds.ls(set_members + shapes, long=True, shapes=True) for shape in all_shapes: if cmds.nodeType(shape) not in cls.accepted_output: invalid.append(shape) @classmethod def validate_controls(cls, set_members): """ Checks if the control set members are allowed node types. Checks if the node types of the set members valid Args: set_members: list of nodes of the controls_set hierarchy: list of nodes which reside under the root node Returns: errors (list) """ # Validate control types invalid = [] for node in set_members: if cmds.nodeType(node) not in cls.accepted_controllers: invalid.append(node) return invalid @classmethod def get_nodes(cls, instance): """Get the target objectsets and rig sets nodes Args: instance (str): instance Returns: tuple: 2-tuple of list of objectsets, list of rig sets nodes """ objectsets = ["controls_SET", "out_SET"] rig_sets_nodes = instance.data.get("rig_sets", []) return objectsets, rig_sets_nodes class ValidateSkeletonRigContents(ValidateRigContents): """Ensure skeleton rigs contains pipeline-critical content The rigs optionally contain at least two object sets: "skeletonMesh_SET" - Set of the skinned meshes with bone hierarchies """ order = ValidateContentsOrder label = "Skeleton Rig Contents" hosts = ["maya"] families = ["rig.fbx"] optional = True @classmethod def get_invalid(cls, instance): objectsets, skeleton_mesh_nodes = cls.get_nodes(instance) cls.validate_missing_objectsets( instance, objectsets, instance.data["rig_sets"]) # Ensure contents in sets and retrieve long path for all objects output_content = instance.data.get("skeleton_mesh", []) output_content = cmds.ls(skeleton_mesh_nodes, long=True) invalid_hierarchy = cls.invalid_hierarchy( instance, output_content) invalid_geometry = cls.validate_geometry(output_content) error = False if invalid_hierarchy: cls.log.error("Found nodes which reside outside of root group " "while they are set up for publishing." "\n%s" % invalid_hierarchy) error = True if invalid_geometry: cls.log.error("Found nodes which reside outside of root group " "while they are set up for publishing." "\n%s" % invalid_hierarchy) error = True if error: return invalid_hierarchy + invalid_geometry @classmethod def get_nodes(cls, instance): """Get the target objectsets and rig sets nodes Args: instance (str): instance Returns: tuple: 2-tuple of list of objectsets, list of rig sets nodes """ objectsets = ["skeletonMesh_SET"] skeleton_mesh_nodes = instance.data.get("skeleton_mesh", []) return objectsets, skeleton_mesh_nodes ================================================ FILE: openpype/hosts/maya/plugins/publish/validate_rig_controllers.py ================================================ from maya import cmds import pyblish.api from openpype.pipeline.publish import ( ValidateContentsOrder, RepairAction, PublishValidationError, OptionalPyblishPluginMixin ) import openpype.hosts.maya.api.action from openpype.hosts.maya.api.lib import undo_chunk class ValidateRigControllers(pyblish.api.InstancePlugin, OptionalPyblishPluginMixin): """Validate rig controllers. Controls must have the transformation attributes on their default values of translate zero, rotate zero and scale one when they are unlocked attributes. Unlocked keyable attributes may not have any incoming connections. If these connections are required for the rig then lock the attributes. The visibility attribute must be locked. Note that `repair` will: - Lock all visibility attributes - Reset all default values for translate, rotate, scale - Break all incoming connections to keyable attributes """ order = ValidateContentsOrder + 0.05 label = "Rig Controllers" hosts = ["maya"] families = ["rig"] optional = True actions = [RepairAction, openpype.hosts.maya.api.action.SelectInvalidAction] # Default controller values CONTROLLER_DEFAULTS = { "translateX": 0, "translateY": 0, "translateZ": 0, "rotateX": 0, "rotateY": 0, "rotateZ": 0, "scaleX": 1, "scaleY": 1, "scaleZ": 1 } def process(self, instance): if not self.is_active(instance.data): return invalid = self.get_invalid(instance) if invalid: raise PublishValidationError( '{} failed, see log information'.format(self.label) ) @classmethod def get_invalid(cls, instance): controls_set = cls.get_node(instance) if not controls_set: cls.log.error( "Must have 'controls_SET' in rig instance" ) return [instance.data["instance_node"]] controls = cmds.sets(controls_set, query=True) # Ensure all controls are within the top group lookup = set(instance[:]) if not all(control in lookup for control in cmds.ls(controls, long=True)): cls.log.error( "All controls must be inside the rig's group." ) return [controls_set] # Validate all controls has_connections = list() has_unlocked_visibility = list() has_non_default_values = list() for control in controls: if cls.get_connected_attributes(control): has_connections.append(control) # check if visibility is locked attribute = "{}.visibility".format(control) locked = cmds.getAttr(attribute, lock=True) if not locked: has_unlocked_visibility.append(control) if cls.get_non_default_attributes(control): has_non_default_values.append(control) if has_connections: cls.log.error("Controls have input connections: " "%s" % has_connections) if has_non_default_values: cls.log.error("Controls have non-default values: " "%s" % has_non_default_values) if has_unlocked_visibility: cls.log.error("Controls have unlocked visibility " "attribute: %s" % has_unlocked_visibility) invalid = [] if (has_connections or has_unlocked_visibility or has_non_default_values): invalid = set() invalid.update(has_connections) invalid.update(has_non_default_values) invalid.update(has_unlocked_visibility) invalid = list(invalid) cls.log.error("Invalid rig controllers. See log for details.") return invalid @classmethod def get_non_default_attributes(cls, control): """Return attribute plugs with non-default values Args: control (str): Name of control node. Returns: list: The invalid plugs """ invalid = [] for attr, default in cls.CONTROLLER_DEFAULTS.items(): if cmds.attributeQuery(attr, node=control, exists=True): plug = "{}.{}".format(control, attr) # Ignore locked attributes locked = cmds.getAttr(plug, lock=True) if locked: continue value = cmds.getAttr(plug) if value != default: cls.log.warning("Control non-default value: " "%s = %s" % (plug, value)) invalid.append(plug) return invalid @staticmethod def get_connected_attributes(control): """Return attribute plugs with incoming connections. This will also ensure no (driven) keys on unlocked keyable attributes. Args: control (str): Name of control node. Returns: list: The invalid plugs """ import maya.cmds as mc # Support controls without any attributes returning None attributes = mc.listAttr(control, keyable=True, scalar=True) or [] invalid = [] for attr in attributes: plug = "{}.{}".format(control, attr) # Ignore locked attributes locked = cmds.getAttr(plug, lock=True) if locked: continue # Ignore proxy connections. if (cmds.addAttr(plug, query=True, exists=True) and cmds.addAttr(plug, query=True, usedAsProxy=True)): continue # Check for incoming connections if cmds.listConnections(plug, source=True, destination=False): invalid.append(plug) return invalid @classmethod def repair(cls, instance): controls_set = cls.get_node(instance) if not controls_set: cls.log.error( "Unable to repair because no 'controls_SET' found in rig " "instance: {}".format(instance) ) return # Use a single undo chunk with undo_chunk(): controls = cmds.sets(controls_set, query=True) for control in controls: # Lock visibility attr = "{}.visibility".format(control) locked = cmds.getAttr(attr, lock=True) if not locked: cls.log.info("Locking visibility for %s" % control) cmds.setAttr(attr, lock=True) # Remove incoming connections invalid_plugs = cls.get_connected_attributes(control) if invalid_plugs: for plug in invalid_plugs: cls.log.info("Breaking input connection to %s" % plug) source = cmds.listConnections(plug, source=True, destination=False, plugs=True)[0] cmds.disconnectAttr(source, plug) # Reset non-default values invalid_plugs = cls.get_non_default_attributes(control) if invalid_plugs: for plug in invalid_plugs: attr = plug.split(".")[-1] default = cls.CONTROLLER_DEFAULTS[attr] cls.log.info("Setting %s to %s" % (plug, default)) cmds.setAttr(plug, default) @classmethod def get_node(cls, instance): """Get target object nodes from controls_SET Args: instance (str): instance Returns: list: list of object nodes from controls_SET """ return instance.data["rig_sets"].get("controls_SET") class ValidateSkeletonRigControllers(ValidateRigControllers): """Validate rig controller for skeletonAnim_SET Controls must have the transformation attributes on their default values of translate zero, rotate zero and scale one when they are unlocked attributes. Unlocked keyable attributes may not have any incoming connections. If these connections are required for the rig then lock the attributes. The visibility attribute must be locked. Note that `repair` will: - Lock all visibility attributes - Reset all default values for translate, rotate, scale - Break all incoming connections to keyable attributes """ order = ValidateContentsOrder + 0.05 label = "Skeleton Rig Controllers" hosts = ["maya"] families = ["rig.fbx"] # Default controller values CONTROLLER_DEFAULTS = { "translateX": 0, "translateY": 0, "translateZ": 0, "rotateX": 0, "rotateY": 0, "rotateZ": 0, "scaleX": 1, "scaleY": 1, "scaleZ": 1 } @classmethod def get_node(cls, instance): """Get target object nodes from skeletonMesh_SET Args: instance (str): instance Returns: list: list of object nodes from skeletonMesh_SET """ return instance.data["rig_sets"].get("skeletonMesh_SET") ================================================ FILE: openpype/hosts/maya/plugins/publish/validate_rig_controllers_arnold_attributes.py ================================================ from maya import cmds import pyblish.api from openpype.pipeline.publish import ( ValidateContentsOrder, RepairAction, PublishValidationError, OptionalPyblishPluginMixin ) from openpype.hosts.maya.api import lib import openpype.hosts.maya.api.action class ValidateRigControllersArnoldAttributes(pyblish.api.InstancePlugin, OptionalPyblishPluginMixin): """Validate rig control curves have no keyable arnold attributes. The Arnold plug-in will create curve attributes like: - aiRenderCurve - aiCurveWidth - aiSampleRate - aiCurveShaderR - aiCurveShaderG - aiCurveShaderB Unfortunately these attributes visible in the channelBox are *keyable* by default and visible in the channelBox. As such pressing a regular "S" set key shortcut will set keys on these attributes too, thus cluttering the animator's scene. This validator will ensure they are hidden or unkeyable attributes. """ order = ValidateContentsOrder + 0.05 label = "Rig Controllers (Arnold Attributes)" hosts = ["maya"] families = ["rig"] optional = False actions = [RepairAction, openpype.hosts.maya.api.action.SelectInvalidAction] attributes = [ "rcurve", "cwdth", "srate", "ai_curve_shaderr", "ai_curve_shaderg", "ai_curve_shaderb" ] def process(self, instance): if not self.is_active(instance.data): return invalid = self.get_invalid(instance) if invalid: raise PublishValidationError('{} failed, see log ' 'information'.format(self.label)) @classmethod def get_invalid(cls, instance): controls_set = instance.data["rig_sets"].get("controls_SET") if not controls_set: return [] controls = cmds.sets(controls_set, query=True) or [] if not controls: return [] shapes = cmds.ls(controls, dag=True, leaf=True, long=True, shapes=True, noIntermediate=True) curves = cmds.ls(shapes, type="nurbsCurve", long=True) invalid = list() for node in curves: for attribute in cls.attributes: if cmds.attributeQuery(attribute, node=node, exists=True): plug = "{}.{}".format(node, attribute) if cmds.getAttr(plug, keyable=True): invalid.append(node) break return invalid @classmethod def repair(cls, instance): invalid = cls.get_invalid(instance) with lib.undo_chunk(): for node in invalid: for attribute in cls.attributes: if cmds.attributeQuery(attribute, node=node, exists=True): plug = "{}.{}".format(node, attribute) cmds.setAttr(plug, channelBox=False, keyable=False) ================================================ FILE: openpype/hosts/maya/plugins/publish/validate_rig_joints_hidden.py ================================================ from maya import cmds import pyblish.api import openpype.hosts.maya.api.action from openpype.hosts.maya.api import lib from openpype.pipeline.publish import ( RepairAction, ValidateContentsOrder, PublishValidationError, OptionalPyblishPluginMixin ) class ValidateRigJointsHidden(pyblish.api.InstancePlugin, OptionalPyblishPluginMixin): """Validate all joints are hidden visually. This includes being hidden: - visibility off, - in a display layer that has visibility off, - having hidden parents or - being an intermediate object. """ order = ValidateContentsOrder hosts = ['maya'] families = ['rig'] label = "Joints Hidden" actions = [openpype.hosts.maya.api.action.SelectInvalidAction, RepairAction] optional = True @staticmethod def get_invalid(instance): joints = cmds.ls(instance, type='joint', long=True) return [j for j in joints if lib.is_visible(j, displayLayer=True)] def process(self, instance): """Process all the nodes in the instance 'objectSet'""" if not self.is_active(instance.data): return invalid = self.get_invalid(instance) if invalid: raise PublishValidationError( "Visible joints found: {0}".format(invalid)) @classmethod def repair(cls, instance): import maya.mel as mel mel.eval("HideJoints") ================================================ FILE: openpype/hosts/maya/plugins/publish/validate_rig_out_set_node_ids.py ================================================ import maya.cmds as cmds import pyblish.api import openpype.hosts.maya.api.action from openpype.hosts.maya.api import lib from openpype.pipeline.publish import ( RepairAction, ValidateContentsOrder, PublishValidationError, OptionalPyblishPluginMixin ) class ValidateRigOutSetNodeIds(pyblish.api.InstancePlugin, OptionalPyblishPluginMixin): """Validate if deformed shapes have related IDs to the original shapes. When a deformer is applied in the scene on a referenced mesh that already had deformers then Maya will create a new shape node for the mesh that does not have the original id. This validator checks whether the ids are valid on all the shape nodes in the instance. """ order = ValidateContentsOrder families = ["rig"] hosts = ['maya'] label = 'Rig Out Set Node Ids' actions = [ openpype.hosts.maya.api.action.SelectInvalidAction, RepairAction ] allow_history_only = False optional = False def process(self, instance): """Process all meshes""" if not self.is_active(instance.data): return # Ensure all nodes have a cbId and a related ID to the original shapes # if a deformer has been created on the shape invalid = self.get_invalid(instance) if invalid: raise PublishValidationError( "Nodes found with mismatching IDs: {0}".format(invalid) ) @classmethod def get_invalid(cls, instance): """Get all nodes which do not match the criteria""" out_set = cls.get_node(instance) if not out_set: return [] invalid = [] members = cmds.sets(out_set, query=True) shapes = cmds.ls(members, dag=True, leaf=True, shapes=True, long=True, noIntermediate=True) for shape in shapes: sibling_id = lib.get_id_from_sibling( shape, history_only=cls.allow_history_only ) if sibling_id: current_id = lib.get_id(shape) if current_id != sibling_id: invalid.append(shape) return invalid @classmethod def repair(cls, instance): for node in cls.get_invalid(instance): # Get the original id from sibling sibling_id = lib.get_id_from_sibling( node, history_only=cls.allow_history_only ) if not sibling_id: cls.log.error("Could not find ID in siblings for '%s'", node) continue lib.set_id(node, sibling_id, overwrite=True) @classmethod def get_node(cls, instance): """Get target object nodes from out_SET Args: instance (str): instance Returns: list: list of object nodes from out_SET """ return instance.data["rig_sets"].get("out_SET") class ValidateSkeletonRigOutSetNodeIds(ValidateRigOutSetNodeIds): """Validate if deformed shapes have related IDs to the original shapes from skeleton set. When a deformer is applied in the scene on a referenced mesh that already had deformers then Maya will create a new shape node for the mesh that does not have the original id. This validator checks whether the ids are valid on all the shape nodes in the instance. """ order = ValidateContentsOrder families = ["rig.fbx"] hosts = ['maya'] label = 'Skeleton Rig Out Set Node Ids' optional = False @classmethod def get_node(cls, instance): """Get target object nodes from skeletonMesh_SET Args: instance (str): instance Returns: list: list of object nodes from skeletonMesh_SET """ return instance.data["rig_sets"].get( "skeletonMesh_SET") ================================================ FILE: openpype/hosts/maya/plugins/publish/validate_rig_output_ids.py ================================================ from collections import defaultdict from maya import cmds import pyblish.api import openpype.hosts.maya.api.action from openpype.hosts.maya.api.lib import get_id, set_id from openpype.pipeline.publish import ( RepairAction, ValidateContentsOrder, PublishValidationError ) def get_basename(node): """Return node short name without namespace""" return node.rsplit("|", 1)[-1].rsplit(":", 1)[-1] class ValidateRigOutputIds(pyblish.api.InstancePlugin): """Validate rig output ids. Ids must share the same id as similarly named nodes in the scene. This is to ensure the id from the model is preserved through animation. """ order = ValidateContentsOrder + 0.05 label = "Rig Output Ids" hosts = ["maya"] families = ["rig"] actions = [RepairAction, openpype.hosts.maya.api.action.SelectInvalidAction] def process(self, instance): invalid = self.get_invalid(instance, compute=True) if invalid: raise PublishValidationError("Found nodes with mismatched IDs.") @classmethod def get_invalid(cls, instance, compute=False): invalid_matches = cls.get_invalid_matches(instance, compute=compute) return list(invalid_matches.keys()) @classmethod def get_invalid_matches(cls, instance, compute=False): invalid = {} if compute: out_set = cls.get_node(instance) if not out_set: instance.data["mismatched_output_ids"] = invalid return invalid instance_nodes = cmds.sets(out_set, query=True, nodesOnly=True) instance_nodes = cmds.ls(instance_nodes, long=True) for node in instance_nodes: shapes = cmds.listRelatives(node, shapes=True, fullPath=True) if shapes: instance_nodes.extend(shapes) scene_nodes = cmds.ls(type="transform", long=True) scene_nodes += cmds.ls(type="mesh", long=True) scene_nodes = set(scene_nodes) - set(instance_nodes) scene_nodes_by_basename = defaultdict(list) for node in scene_nodes: basename = get_basename(node) scene_nodes_by_basename[basename].append(node) for instance_node in instance_nodes: basename = get_basename(instance_node) if basename not in scene_nodes_by_basename: continue matches = scene_nodes_by_basename[basename] ids = set(get_id(node) for node in matches) ids.add(get_id(instance_node)) if len(ids) > 1: cls.log.error( "\"{}\" id mismatch to: {}".format( instance_node, matches ) ) invalid[instance_node] = matches instance.data["mismatched_output_ids"] = invalid else: invalid = instance.data["mismatched_output_ids"] return invalid @classmethod def repair(cls, instance): invalid_matches = cls.get_invalid_matches(instance) multiple_ids_match = [] for instance_node, matches in invalid_matches.items(): ids = set(get_id(node) for node in matches) # If there are multiple scene ids matched, and error needs to be # raised for manual correction. if len(ids) > 1: multiple_ids_match.append({"node": instance_node, "matches": matches}) continue id_to_set = next(iter(ids)) set_id(instance_node, id_to_set, overwrite=True) if multiple_ids_match: raise PublishValidationError( "Multiple matched ids found. Please repair manually: " "{}".format(multiple_ids_match) ) @classmethod def get_node(cls, instance): """Get target object nodes from out_SET Args: instance (str): instance Returns: list: list of object nodes from out_SET """ return instance.data["rig_sets"].get("out_SET") class ValidateSkeletonRigOutputIds(ValidateRigOutputIds): """Validate rig output ids from the skeleton sets. Ids must share the same id as similarly named nodes in the scene. This is to ensure the id from the model is preserved through animation. """ order = ValidateContentsOrder + 0.05 label = "Skeleton Rig Output Ids" hosts = ["maya"] families = ["rig.fbx"] @classmethod def get_node(cls, instance): """Get target object nodes from skeletonMesh_SET Args: instance (str): instance Returns: list: list of object nodes from skeletonMesh_SET """ return instance.data["rig_sets"].get("skeletonMesh_SET") ================================================ FILE: openpype/hosts/maya/plugins/publish/validate_scene_set_workspace.py ================================================ import os import maya.cmds as cmds import pyblish.api from openpype.pipeline.publish import ( PublishValidationError, ValidatePipelineOrder) def is_subdir(path, root_dir): """ Returns whether path is a subdirectory (or file) within root_dir """ path = os.path.realpath(path) root_dir = os.path.realpath(root_dir) # If not on same drive if os.path.splitdrive(path)[0].lower() != os.path.splitdrive(root_dir)[0].lower(): # noqa: E501 return False # Get 'relative path' (can contain ../ which means going up) relative = os.path.relpath(path, root_dir) # Check if the path starts by going up, if so it's not a subdirectory. :) if relative.startswith(os.pardir) or relative == os.curdir: return False else: return True class ValidateSceneSetWorkspace(pyblish.api.ContextPlugin): """Validate the scene is inside the currently set Maya workspace""" order = ValidatePipelineOrder hosts = ['maya'] label = 'Maya Workspace Set' def process(self, context): scene_name = cmds.file(query=True, sceneName=True) if not scene_name: raise PublishValidationError( "Scene hasn't been saved. Workspace can't be validated.") root_dir = cmds.workspace(query=True, rootDirectory=True) if not is_subdir(scene_name, root_dir): raise PublishValidationError( "Maya workspace is not set correctly.\n\n" f"Current workfile `{scene_name}` is not inside the " "current Maya project root directory `{root_dir}`.\n\n" "Please use Workfile app to re-save." ) ================================================ FILE: openpype/hosts/maya/plugins/publish/validate_setdress_root.py ================================================ import pyblish.api from openpype.pipeline.publish import ValidateContentsOrder class ValidateSetdressRoot(pyblish.api.InstancePlugin): """Validate if set dress top root node is published.""" order = ValidateContentsOrder label = "SetDress Root" hosts = ["maya"] families = ["setdress"] def process(self, instance): from maya import cmds if instance.data.get("exactSetMembersOnly"): return set_member = instance.data["setMembers"] root = cmds.ls(set_member, assemblies=True, long=True) if not root or root[0] not in set_member: raise Exception("Setdress top root node is not being published.") ================================================ FILE: openpype/hosts/maya/plugins/publish/validate_shader_name.py ================================================ import re import pyblish.api from maya import cmds import openpype.hosts.maya.api.action from openpype.pipeline.publish import ( OptionalPyblishPluginMixin, PublishValidationError, ValidateContentsOrder) class ValidateShaderName(pyblish.api.InstancePlugin, OptionalPyblishPluginMixin): """Validate shader name assigned. It should be _<*>_SHD """ optional = True order = ValidateContentsOrder families = ["look"] hosts = ['maya'] label = 'Validate Shaders Name' actions = [openpype.hosts.maya.api.action.SelectInvalidAction] regex = r'(?P .*)_(.*)_SHD' # The default connections to check def process(self, instance): if not self.is_active(instance.data): return invalid = self.get_invalid(instance) if invalid: raise PublishValidationError( ("Found shapes with invalid shader names " "assigned:\n{}").format(invalid)) @classmethod def get_invalid(cls, instance): invalid = [] # Get all shapes from the instance content_instance = instance.data.get("setMembers", None) if not content_instance: cls.log.error("Instance has no nodes!") return True pass descendants = cmds.listRelatives(content_instance, allDescendents=True, fullPath=True) or [] descendants = cmds.ls(descendants, noIntermediate=True, long=True) shapes = cmds.ls(descendants, type=["nurbsSurface", "mesh"], long=True) asset_name = instance.data.get("asset") # Check the number of connected shadingEngines per shape regex_compile = re.compile(cls.regex) error_message = "object {0} has invalid shader name {1}" for shape in shapes: shading_engines = cmds.listConnections(shape, destination=True, type="shadingEngine") or [] shaders = cmds.ls( cmds.listConnections(shading_engines), materials=1 ) for shader in shaders: m = regex_compile.match(shader) if m is None: invalid.append(shape) cls.log.error(error_message.format(shape, shader)) else: if 'asset' in regex_compile.groupindex: if m.group('asset') != asset_name: invalid.append(shape) message = error_message message += " with missing asset name \"{2}\"" cls.log.error( message.format(shape, shader, asset_name) ) return invalid ================================================ FILE: openpype/hosts/maya/plugins/publish/validate_shape_default_names.py ================================================ import re from maya import cmds import pyblish.api import openpype.hosts.maya.api.action from openpype.pipeline.publish import ( ValidateContentsOrder, RepairAction, OptionalPyblishPluginMixin ) def short_name(node): return node.rsplit("|", 1)[-1].rsplit(":", 1)[-1] class ValidateShapeDefaultNames(pyblish.api.InstancePlugin, OptionalPyblishPluginMixin): """Validates that Shape names are using Maya's default format. When you create a new polygon cube Maya will name the transform and shape respectively: - ['pCube1', 'pCubeShape1'] If you rename it to `bar1` it will become: - ['bar1', 'barShape1'] Then if you rename it to `bar` it will become: - ['bar', 'barShape'] Rename it again to `bar1` it will differ as opposed to before: - ['bar1', 'bar1Shape'] Note that bar1Shape != barShape1 Thus the suffix number can be either in front of Shape or behind it. Then it becomes harder to define where what number should be when a node contains multiple shapes, for example with many controls in rigs existing of multiple curves. """ order = ValidateContentsOrder hosts = ['maya'] families = ['model'] optional = True label = "Shape Default Naming" actions = [openpype.hosts.maya.api.action.SelectInvalidAction, RepairAction] @staticmethod def _define_default_name(shape): parent = cmds.listRelatives(shape, parent=True, fullPath=True)[0] transform = short_name(parent) return '{0}Shape'.format(transform) @staticmethod def _is_valid(shape): """ Return whether the shape's name is similar to Maya's default. """ transform = cmds.listRelatives(shape, parent=True, fullPath=True)[0] transform_name = short_name(transform) shape_name = short_name(shape) # A Shape's name can be either {transform}{numSuffix} # Shape or {transform}Shape{numSuffix} # Upon renaming nodes in Maya that is # the pattern Maya will act towards. transform_no_num = transform_name.rstrip("0123456789") pattern = '^{transform}[0-9]*Shape[0-9]*$'.format( transform=transform_no_num) if re.match(pattern, shape_name): return True else: return False @classmethod def get_invalid(cls, instance): shapes = cmds.ls(instance, shapes=True, long=True) return [shape for shape in shapes if not cls._is_valid(shape)] def process(self, instance): """Process all the shape nodes in the instance""" if not self.is_active(instance.data): return invalid = self.get_invalid(instance) if invalid: raise ValueError("Incorrectly named shapes " "found: {0}".format(invalid)) @classmethod def repair(cls, instance): """Process all the shape nodes in the instance""" for shape in cls.get_invalid(instance): correct_shape_name = cls._define_default_name(shape) cmds.rename(shape, correct_shape_name) ================================================ FILE: openpype/hosts/maya/plugins/publish/validate_shape_render_stats.py ================================================ import pyblish.api from maya import cmds import openpype.hosts.maya.api.action from openpype.pipeline.publish import ( RepairAction, ValidateMeshOrder, OptionalPyblishPluginMixin ) class ValidateShapeRenderStats(pyblish.api.Validator, OptionalPyblishPluginMixin): """Ensure all render stats are set to the default values.""" order = ValidateMeshOrder hosts = ['maya'] families = ['model'] label = 'Shape Default Render Stats' actions = [openpype.hosts.maya.api.action.SelectInvalidAction, RepairAction] optional = True defaults = {'castsShadows': 1, 'receiveShadows': 1, 'motionBlur': 1, 'primaryVisibility': 1, 'smoothShading': 1, 'visibleInReflections': 1, 'visibleInRefractions': 1, 'doubleSided': 1, 'opposite': 0} @classmethod def get_invalid(cls, instance): # It seems the "surfaceShape" and those derived from it have # `renderStat` attributes. shapes = cmds.ls(instance, long=True, type='surfaceShape') invalid = [] for shape in shapes: _iteritems = getattr(cls.defaults, "iteritems", cls.defaults.items) for attr, default_value in _iteritems(): if cmds.attributeQuery(attr, node=shape, exists=True): value = cmds.getAttr('{}.{}'.format(shape, attr)) if value != default_value: invalid.append(shape) return invalid def process(self, instance): if not self.is_active(instance.data): return invalid = self.get_invalid(instance) if invalid: raise ValueError("Shapes with non-default renderStats " "found: {0}".format(invalid)) @classmethod def repair(cls, instance): for shape in cls.get_invalid(instance): _iteritems = getattr(cls.defaults, "iteritems", cls.defaults.items) for attr, default_value in _iteritems(): if cmds.attributeQuery(attr, node=shape, exists=True): plug = '{0}.{1}'.format(shape, attr) value = cmds.getAttr(plug) if value != default_value: cmds.setAttr(plug, default_value) ================================================ FILE: openpype/hosts/maya/plugins/publish/validate_shape_zero.py ================================================ from maya import cmds import pyblish.api import openpype.hosts.maya.api.action from openpype.hosts.maya.api import lib from openpype.pipeline.publish import ( ValidateContentsOrder, RepairAction, PublishValidationError, OptionalPyblishPluginMixin ) class ValidateShapeZero(pyblish.api.Validator, OptionalPyblishPluginMixin): """Shape components may not have any "tweak" values To solve this issue, try freezing the shapes. """ order = ValidateContentsOrder hosts = ["maya"] families = ["model"] label = "Shape Zero (Freeze)" actions = [ openpype.hosts.maya.api.action.SelectInvalidAction, RepairAction ] optional = True @staticmethod def get_invalid(instance): """Returns the invalid shapes in the instance. This is the same as checking: - all(pnt == [0,0,0] for pnt in shape.pnts[:]) Returns: list: Shape with non freezed vertex """ shapes = cmds.ls(instance, type="shape") invalid = [] for shape in shapes: if cmds.polyCollapseTweaks(shape, q=True, hasVertexTweaks=True): invalid.append(shape) return invalid @classmethod def repair(cls, instance): invalid_shapes = cls.get_invalid(instance) if not invalid_shapes: return with lib.maintained_selection(): with lib.tool("selectSuperContext"): for shape in invalid_shapes: cmds.polyCollapseTweaks(shape) # cmds.polyCollapseTweaks keeps selecting the geometry # after each command. When running on many meshes # after one another this tends to get really heavy cmds.select(clear=True) def process(self, instance): """Process all the nodes in the instance "objectSet""" if not self.is_active(instance.data): return invalid = self.get_invalid(instance) if invalid: raise PublishValidationError( title="Shape Component Tweaks", message="Shapes found with non-zero component tweaks: '{}'" "".format(", ".join(invalid)), description=( "## Shapes found with component tweaks\n" "Shapes were detected that have component tweaks on their " "components. Please remove the component tweaks to " "continue.\n\n" "### Repair\n" "The repair action will try to *freeze* the component " "tweaks into the shapes, which is usually the correct fix " "if the mesh has no construction history (= has its " "history deleted)."), detail=( "Maya allows to store component tweaks within shape nodes " "which are applied between its `inMesh` and `outMesh` " "connections resulting in the output of a shape node " "differing from the input. We usually want to avoid this " "for published meshes (in particular for Maya scenes) as " "it can have unintended results when using these meshes " "as intermediate meshes since it applies positional " "differences without being visible edits in the node " "graph.\n\n" "These tweaks are traditionally stored in the `.pnts` " "attribute of shapes.") ) ================================================ FILE: openpype/hosts/maya/plugins/publish/validate_single_assembly.py ================================================ import pyblish.api from openpype.pipeline.publish import ValidateContentsOrder class ValidateSingleAssembly(pyblish.api.InstancePlugin): """Ensure the content of the instance is grouped in a single hierarchy The instance must have a single root node containing all the content. This root node *must* be a top group in the outliner. Example outliner: root_GRP -- geometry_GRP -- mesh_GEO -- controls_GRP -- control_CTL """ order = ValidateContentsOrder hosts = ['maya'] families = ['rig'] label = 'Single Assembly' def process(self, instance): from maya import cmds assemblies = cmds.ls(instance, assemblies=True) # ensure unique (somehow `maya.cmds.ls` doesn't manage that) assemblies = set(assemblies) assert len(assemblies) > 0, ( "One assembly required for: %s (currently empty?)" % instance) assert len(assemblies) < 2, ( 'Multiple assemblies found: %s' % assemblies) ================================================ FILE: openpype/hosts/maya/plugins/publish/validate_skeletalmesh_hierarchy.py ================================================ # -*- coding: utf-8 -*- import pyblish.api from openpype.pipeline.publish import ( ValidateContentsOrder, PublishXmlValidationError, OptionalPyblishPluginMixin ) from maya import cmds class ValidateSkeletalMeshHierarchy(pyblish.api.InstancePlugin, OptionalPyblishPluginMixin): """Validates that nodes has common root.""" order = ValidateContentsOrder hosts = ["maya"] families = ["skeletalMesh"] label = "Skeletal Mesh Top Node" optional = False def process(self, instance): if not self.is_active(instance.data): return geo = instance.data.get("geometry") joints = instance.data.get("joints") joints_parents = cmds.ls(joints, long=True) geo_parents = cmds.ls(geo, long=True) parents_set = { parent.split("|")[1] for parent in (joints_parents + geo_parents) } self.log.debug(parents_set) if len(set(parents_set)) > 2: raise PublishXmlValidationError( self, "Multiple roots on geometry or joints." ) ================================================ FILE: openpype/hosts/maya/plugins/publish/validate_skeletalmesh_triangulated.py ================================================ # -*- coding: utf-8 -*- import pyblish.api from openpype.hosts.maya.api.action import ( SelectInvalidAction, ) from openpype.pipeline.publish import ( RepairAction, ValidateContentsOrder, PublishValidationError ) from maya import cmds class ValidateSkeletalMeshTriangulated(pyblish.api.InstancePlugin): """Validates that the geometry has been triangulated.""" order = ValidateContentsOrder hosts = ["maya"] families = ["skeletalMesh"] label = "Skeletal Mesh Triangulated" optional = True actions = [ SelectInvalidAction, RepairAction ] def process(self, instance): invalid = self.get_invalid(instance) if invalid: raise PublishValidationError( "The following objects needs to be triangulated: " "{}".format(invalid)) @classmethod def get_invalid(cls, instance): geo = instance.data.get("geometry") invalid = [] for obj in cmds.listRelatives( cmds.ls(geo), allDescendents=True, fullPath=True): n_triangles = cmds.polyEvaluate(obj, triangle=True) n_faces = cmds.polyEvaluate(obj, face=True) if not (isinstance(n_triangles, int) and isinstance(n_faces, int)): continue # We check if the number of triangles is equal to the number of # faces for each transform node. # If it is, the object is triangulated. if cmds.objectType(obj, i="transform") and n_triangles != n_faces: invalid.append(obj) return invalid @classmethod def repair(cls, instance): for node in cls.get_invalid(instance): cmds.polyTriangulate(node) ================================================ FILE: openpype/hosts/maya/plugins/publish/validate_skeleton_top_group_hierarchy.py ================================================ # -*- coding: utf-8 -*- """Plugin for validating naming conventions.""" from maya import cmds import pyblish.api from openpype.pipeline.publish import ( ValidateContentsOrder, OptionalPyblishPluginMixin, PublishValidationError ) class ValidateSkeletonTopGroupHierarchy(pyblish.api.InstancePlugin, OptionalPyblishPluginMixin): """Validates top group hierarchy in the SETs Make sure the object inside the SETs are always top group of the hierarchy """ order = ValidateContentsOrder + 0.05 label = "Skeleton Rig Top Group Hierarchy" families = ["rig.fbx"] def process(self, instance): invalid = [] skeleton_mesh_data = instance.data("skeleton_mesh", []) if skeleton_mesh_data: invalid = self.get_top_hierarchy(skeleton_mesh_data) if invalid: raise PublishValidationError( "The skeletonMesh_SET includes the object which " "is not at the top hierarchy: {}".format(invalid)) def get_top_hierarchy(self, targets): targets = cmds.ls(targets, long=True) # ensure long names non_top_hierarchy_list = [ target for target in targets if target.count("|") > 2 ] return non_top_hierarchy_list ================================================ FILE: openpype/hosts/maya/plugins/publish/validate_skinCluster_deformer_set.py ================================================ from maya import cmds import pyblish.api import openpype.hosts.maya.api.action from openpype.pipeline.publish import ( ValidateContentsOrder, OptionalPyblishPluginMixin ) class ValidateSkinclusterDeformerSet(pyblish.api.InstancePlugin, OptionalPyblishPluginMixin): """Validate skinClusters on meshes have valid member relationships. In rare cases it can happen that a mesh has a skinCluster in its history but it is *not* included in the deformer relationship history. If this is the case then FBX will not export the skinning. """ order = ValidateContentsOrder hosts = ['maya'] families = ['fbx'] label = "Skincluster Deformer Relationships" actions = [openpype.hosts.maya.api.action.SelectInvalidAction] optional = False def process(self, instance): """Process all the transform nodes in the instance""" if not self.is_active(instance.data): return invalid = self.get_invalid(instance) if invalid: raise ValueError("Invalid skinCluster relationships " "found on meshes: {0}".format(invalid)) @classmethod def get_invalid(cls, instance): meshes = cmds.ls(instance, type="mesh", noIntermediate=True, long=True) invalid = list() for mesh in meshes: history = cmds.listHistory(mesh) or [] skins = cmds.ls(history, type="skinCluster") # Ensure at most one skinCluster assert len(skins) <= 1, "Cannot have more than one skinCluster" if skins: skin = skins[0] # Ensure the mesh is also in the skinCluster set # otherwise the skin will not be exported correctly # by the FBX Exporter. deformer_sets = cmds.listSets(object=mesh, type=2) for deformer_set in deformer_sets: used_by = cmds.listConnections(deformer_set + ".usedBy", source=True, destination=False) # Ignore those that don't seem to have a usedBy connection if not used_by: continue # We have a matching deformer set relationship if skin in set(used_by): break else: invalid.append(mesh) cls.log.warning( "Mesh has skinCluster in history but is not included " "in its deformer relationship set: " "{0} (skinCluster: {1})".format(mesh, skin) ) return invalid ================================================ FILE: openpype/hosts/maya/plugins/publish/validate_step_size.py ================================================ import pyblish.api import openpype.hosts.maya.api.action from openpype.pipeline.publish import ( PublishValidationError, ValidateContentsOrder, OptionalPyblishPluginMixin ) class ValidateStepSize(pyblish.api.InstancePlugin, OptionalPyblishPluginMixin): """Validates the step size for the instance is in a valid range. For example the `step` size should never be lower or equal to zero. """ order = ValidateContentsOrder label = 'Step size' families = ['camera', 'pointcache', 'animation'] actions = [openpype.hosts.maya.api.action.SelectInvalidAction] optional = False MIN = 0.01 MAX = 1.0 @classmethod def get_invalid(cls, instance): objset = instance.data['name'] step = instance.data.get("step", 1.0) if step < cls.MIN or step > cls.MAX: cls.log.warning("Step size is outside of valid range: {0} " "(valid: {1} to {2})".format(step, cls.MIN, cls.MAX)) return objset return [] def process(self, instance): if not self.is_active(instance.data): return invalid = self.get_invalid(instance) if invalid: raise PublishValidationError( "Invalid instances found: {0}".format(invalid)) ================================================ FILE: openpype/hosts/maya/plugins/publish/validate_transform_naming_suffix.py ================================================ # -*- coding: utf-8 -*- """Plugin for validating naming conventions.""" from maya import cmds import pyblish.api import openpype.hosts.maya.api.action from openpype.pipeline.publish import ( ValidateContentsOrder, OptionalPyblishPluginMixin, PublishValidationError ) class ValidateTransformNamingSuffix(pyblish.api.InstancePlugin, OptionalPyblishPluginMixin): """Validates transform suffix based on the type of its children shapes. Suffices must be: - mesh: _GEO (regular geometry) _GES (geometry to be smoothed at render) _GEP (proxy geometry; usually not to be rendered) _OSD (open subdiv smooth at rendertime) - nurbsCurve: _CRV - nurbsSurface: _NRB - locator: _LOC - null/group: _GRP Suffices can also be overridden by project settings. .. warning:: This grabs the first child shape as a reference and doesn't use the others in the check. """ order = ValidateContentsOrder hosts = ['maya'] families = ['model'] optional = True label = 'Suffix Naming Conventions' actions = [openpype.hosts.maya.api.action.SelectInvalidAction] SUFFIX_NAMING_TABLE = {"mesh": ["_GEO", "_GES", "_GEP", "_OSD"], "nurbsCurve": ["_CRV"], "nurbsSurface": ["_NRB"], "locator": ["_LOC"], "group": ["_GRP"]} ALLOW_IF_NOT_IN_SUFFIX_TABLE = True @classmethod def get_table_for_invalid(cls): ss = [] for k, v in cls.SUFFIX_NAMING_TABLE.items(): ss.append(" - {}: {}".format(k, ", ".join(v))) return "
".join(ss) @staticmethod def is_valid_name(node_name, shape_type, SUFFIX_NAMING_TABLE, ALLOW_IF_NOT_IN_SUFFIX_TABLE): """Return whether node's name is correct. The correctness for a transform's suffix is dependent on what `shape_type` it holds. E.g. a transform with a mesh might need and `_GEO` suffix. When `shape_type` is None the transform doesn't have any direct children shapes. Args: node_name (str): Node name. shape_type (str): Type of node. SUFFIX_NAMING_TABLE (dict): Mapping dict for suffixes. ALLOW_IF_NOT_IN_SUFFIX_TABLE (dict): Filter dict. """ if shape_type not in SUFFIX_NAMING_TABLE: return ALLOW_IF_NOT_IN_SUFFIX_TABLE else: suffices = SUFFIX_NAMING_TABLE[shape_type] for suffix in suffices: if node_name.endswith(suffix): return True return False @classmethod def get_invalid(cls, instance): """Get invalid nodes in instance. Args: instance (:class:`pyblish.api.Instance`): published instance. """ transforms = cmds.ls(instance, type='transform', long=True) invalid = [] for transform in transforms: shapes = cmds.listRelatives(transform, shapes=True, fullPath=True, noIntermediate=True) shape_type = cmds.nodeType(shapes[0]) if shapes else "group" if not cls.is_valid_name(transform, shape_type, cls.SUFFIX_NAMING_TABLE, cls.ALLOW_IF_NOT_IN_SUFFIX_TABLE): invalid.append(transform) return invalid def process(self, instance): """Process all the nodes in the instance. Args: instance (:class:`pyblish.api.Instance`): published instance. """ if not self.is_active(instance.data): return invalid = self.get_invalid(instance) if invalid: valid = self.get_table_for_invalid() names = "
".join( " - {}".format(node) for node in invalid ) valid = valid.replace("\n", "
") raise PublishValidationError( title="Invalid naming suffix", message="Valid suffixes are:
{0}
" "Incorrectly named geometry transforms:
{1}" "".format(valid, names)) ================================================ FILE: openpype/hosts/maya/plugins/publish/validate_transform_zero.py ================================================ from maya import cmds import pyblish.api import openpype.hosts.maya.api.action from openpype.pipeline.publish import ( ValidateContentsOrder, PublishValidationError, OptionalPyblishPluginMixin ) class ValidateTransformZero(pyblish.api.Validator, OptionalPyblishPluginMixin): """Transforms can't have any values To solve this issue, try freezing the transforms. So long as the transforms, rotation and scale values are zero, you're all good. """ order = ValidateContentsOrder hosts = ["maya"] families = ["model"] label = "Transform Zero (Freeze)" actions = [openpype.hosts.maya.api.action.SelectInvalidAction] _identity = [1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0] _tolerance = 1e-30 optional = True @classmethod def get_invalid(cls, instance): """Returns the invalid transforms in the instance. This is the same as checking: - translate == [0, 0, 0] and rotate == [0, 0, 0] and scale == [1, 1, 1] and shear == [0, 0, 0] .. note:: This will also catch camera transforms if those are in the instances. Returns: list: Transforms that are not identity matrix """ transforms = cmds.ls(instance, type="transform") invalid = [] for transform in transforms: if ('_LOC' in transform) or ('_loc' in transform): continue mat = cmds.xform(transform, q=1, matrix=True, objectSpace=True) if not all(abs(x-y) < cls._tolerance for x, y in zip(cls._identity, mat)): invalid.append(transform) return invalid def process(self, instance): """Process all the nodes in the instance "objectSet""" if not self.is_active(instance.data): return invalid = self.get_invalid(instance) if invalid: names = "
".join( " - {}".format(node) for node in invalid ) raise PublishValidationError( title="Transform Zero", message="The model publish allows no transformations. You must" " freeze transformations to continue.
" "Nodes found with transform values: " "{0}".format(names)) ================================================ FILE: openpype/hosts/maya/plugins/publish/validate_unique_names.py ================================================ from maya import cmds import pyblish.api import openpype.hosts.maya.api.action from openpype.pipeline.publish import ( ValidateContentsOrder, OptionalPyblishPluginMixin ) class ValidateUniqueNames(pyblish.api.Validator, OptionalPyblishPluginMixin): """transform names should be unique ie: using cmds.ls(someNodeName) should always return shortname """ order = ValidateContentsOrder hosts = ["maya"] families = ["model"] label = "Unique transform name" actions = [openpype.hosts.maya.api.action.SelectInvalidAction] optional = True @staticmethod def get_invalid(instance): """Returns the invalid transforms in the instance. Returns: list: Non-unique name transforms. """ return [tr for tr in cmds.ls(instance, type="transform") if '|' in tr] def process(self, instance): """Process all the nodes in the instance "objectSet""" if not self.is_active(instance.data): return invalid = self.get_invalid(instance) if invalid: raise ValueError("Nodes found with none unique names. " "values: {0}".format(invalid)) ================================================ FILE: openpype/hosts/maya/plugins/publish/validate_unreal_mesh_triangulated.py ================================================ # -*- coding: utf-8 -*- from maya import cmds import pyblish.api from openpype.pipeline.publish import ( ValidateMeshOrder, OptionalPyblishPluginMixin ) import openpype.hosts.maya.api.action class ValidateUnrealMeshTriangulated(pyblish.api.InstancePlugin, OptionalPyblishPluginMixin): """Validate if mesh is made of triangles for Unreal Engine""" order = ValidateMeshOrder hosts = ["maya"] families = ["staticMesh"] label = "Mesh is Triangulated" actions = [openpype.hosts.maya.api.action.SelectInvalidAction] active = False @classmethod def get_invalid(cls, instance): invalid = [] meshes = cmds.ls(instance, type="mesh", long=True) for mesh in meshes: faces = cmds.polyEvaluate(mesh, f=True) tris = cmds.polyEvaluate(mesh, t=True) if faces != tris: invalid.append(mesh) return invalid def process(self, instance): if not self.is_active(instance.data): return invalid = self.get_invalid(instance) assert len(invalid) == 0, ( "Found meshes without triangles") ================================================ FILE: openpype/hosts/maya/plugins/publish/validate_unreal_staticmesh_naming.py ================================================ # -*- coding: utf-8 -*- """Validator for correct naming of Static Meshes.""" import re import pyblish.api import openpype.hosts.maya.api.action from openpype.pipeline import legacy_io from openpype.settings import get_project_settings from openpype.pipeline.publish import ( ValidateContentsOrder, OptionalPyblishPluginMixin, PublishValidationError ) class ValidateUnrealStaticMeshName(pyblish.api.InstancePlugin, OptionalPyblishPluginMixin): """Validate name of Unreal Static Mesh Unreals naming convention states that staticMesh should start with `SM` prefix - SM_[Name]_## (Eg. SM_sube_01).These prefixes can be configured in Settings UI. This plugin also validates other types of meshes - collision meshes: UBX_[RenderMeshName]*: Boxes are created with the Box objects type in Max or with the Cube polygonal primitive in Maya. You cannot move the vertices around or deform it in any way to make it something other than a rectangular prism, or else it will not work. UCP_[RenderMeshName]*: Capsules are created with the Capsule object type. The capsule does not need to have many segments (8 is a good number) at all because it is converted into a true capsule for collision. Like boxes, you should not move the individual vertices around. USP_[RenderMeshName]*: Spheres are created with the Sphere object type. The sphere does not need to have many segments (8 is a good number) at all because it is converted into a true sphere for collision. Like boxes, you should not move the individual vertices around. UCX_[RenderMeshName]*: Convex objects can be any completely closed convex 3D shape. For example, a box can also be a convex object This validator also checks if collision mesh [RenderMeshName] matches one of SM_[RenderMeshName]. """ optional = True order = ValidateContentsOrder hosts = ["maya"] families = ["staticMesh"] label = "Unreal Static Mesh Name" actions = [openpype.hosts.maya.api.action.SelectInvalidAction] regex_mesh = r"(?P.*))" regex_collision = r"(?P .*)" @classmethod def get_invalid(cls, instance): invalid = [] collision_prefixes = ( instance.context.data["project_settings"] ["maya"] ["create"] ["CreateUnrealStaticMesh"] ["collision_prefixes"] ) if cls.validate_mesh: # compile regex for testing names regex_mesh = "{}{}".format( ("_" + cls.static_mesh_prefix) or "", cls.regex_mesh ) sm_r = re.compile(regex_mesh) if not sm_r.match(instance.data.get("subset")): cls.log.error("Mesh doesn't comply with name validation.") return True if cls.validate_collision: collision_set = instance.data.get("collisionMembers", None) # soft-fail is there are no collision objects if not collision_set: cls.log.warning("No collision objects to validate.") return False regex_collision = "{}{}_(\\d+)".format( "(?P ({}))_".format( "|".join("{0}".format(p) for p in collision_prefixes) ) or "", cls.regex_collision ) cl_r = re.compile(regex_collision) asset_name = instance.data["assetEntity"]["name"] mesh_name = "{}{}".format(asset_name, instance.data.get("variant", [])) for obj in collision_set: cl_m = cl_r.match(obj) if not cl_m: cls.log.error("{} is invalid".format(obj)) invalid.append(obj) else: expected_collision = "{}_{}".format( cl_m.group("prefix"), mesh_name ) if not obj.startswith(expected_collision): cls.log.error( "Collision object name doesn't match " "static mesh name" ) cls.log.error("{}_{} != {}_{}*".format( cl_m.group("prefix"), cl_m.group("renderName"), cl_m.group("prefix"), mesh_name, )) invalid.append(obj) return invalid def process(self, instance): if not self.is_active(instance.data): return if not self.validate_mesh and not self.validate_collision: self.log.debug("Validation of both mesh and collision names" "is disabled.") return if not instance.data.get("collisionMembers", None): self.log.debug("There are no collision objects to validate") return invalid = self.get_invalid(instance) if invalid: raise PublishValidationError("Model naming is invalid. See log.") ================================================ FILE: openpype/hosts/maya/plugins/publish/validate_unreal_up_axis.py ================================================ # -*- coding: utf-8 -*- from maya import cmds import pyblish.api from openpype.pipeline.publish import ( ValidateContentsOrder, RepairAction, OptionalPyblishPluginMixin ) class ValidateUnrealUpAxis(pyblish.api.ContextPlugin, OptionalPyblishPluginMixin): """Validate if Z is set as up axis in Maya""" optional = True active = False order = ValidateContentsOrder hosts = ["maya"] families = ["staticMesh"] label = "Unreal Up-Axis check" actions = [RepairAction] def process(self, context): if not self.is_active(context.data): return assert cmds.upAxis(q=True, axis=True) == "z", ( "Invalid axis set as up axis" ) @classmethod def repair(cls, instance): cmds.upAxis(axis="z", rotateView=True) ================================================ FILE: openpype/hosts/maya/plugins/publish/validate_visible_only.py ================================================ import pyblish.api from openpype.hosts.maya.api.lib import iter_visible_nodes_in_range import openpype.hosts.maya.api.action from openpype.pipeline.publish import ( ValidateContentsOrder, PublishValidationError, OptionalPyblishPluginMixin ) class ValidateAlembicVisibleOnly(pyblish.api.InstancePlugin, OptionalPyblishPluginMixin): """Validates at least a single node is visible in frame range. This validation only validates if the `visibleOnly` flag is enabled on the instance - otherwise the validation is skipped. """ order = ValidateContentsOrder + 0.05 label = "Alembic Visible Only" hosts = ["maya"] families = ["pointcache", "animation"] actions = [openpype.hosts.maya.api.action.SelectInvalidAction] optional = False def process(self, instance): if not self.is_active(instance.data): return if not instance.data.get("visibleOnly", False): self.log.debug("Visible only is disabled. Validation skipped..") return invalid = self.get_invalid(instance) if invalid: start, end = self.get_frame_range(instance) raise PublishValidationError("No visible nodes found in " "frame range {}-{}.".format(start, end)) @classmethod def get_invalid(cls, instance): if instance.data["family"] == "animation": # Special behavior to use the nodes in out_SET nodes = instance.data["out_hierarchy"] else: nodes = instance[:] start, end = cls.get_frame_range(instance) if not any(iter_visible_nodes_in_range(nodes, start, end)): # Return the nodes we have considered so the user can identify # them with the select invalid action return nodes @staticmethod def get_frame_range(instance): data = instance.data return data["frameStartHandle"], data["frameEndHandle"] ================================================ FILE: openpype/hosts/maya/plugins/publish/validate_vray.py ================================================ from maya import cmds import pyblish.api from openpype.pipeline.publish import PublishValidationError class ValidateVray(pyblish.api.InstancePlugin): """Validate general Vray setup.""" order = pyblish.api.ValidatorOrder label = 'VRay' hosts = ["maya"] families = ["vrayproxy"] def process(self, instance): # Validate vray plugin is loaded. if not cmds.pluginInfo("vrayformaya", query=True, loaded=True): raise PublishValidationError("Vray plugin is not loaded.") ================================================ FILE: openpype/hosts/maya/plugins/publish/validate_vray_distributed_rendering.py ================================================ import pyblish.api from maya import cmds from openpype.hosts.maya.api import lib from openpype.pipeline.publish import ( PublishValidationError, RepairAction, ValidateContentsOrder, OptionalPyblishPluginMixin ) class ValidateVRayDistributedRendering(pyblish.api.InstancePlugin, OptionalPyblishPluginMixin): """Validate V-Ray Distributed Rendering is ignored in batch mode. Whenever Distributed Rendering is enabled for V-Ray in the render settings ensure that the "Ignore in batch mode" is enabled so the submitted job won't try to render each frame with all machines resulting in faulty errors. """ order = ValidateContentsOrder label = "VRay Distributed Rendering" families = ["renderlayer"] actions = [RepairAction] optional = False # V-Ray attribute names enabled_attr = "vraySettings.sys_distributed_rendering_on" ignored_attr = "vraySettings.sys_distributed_rendering_ignore_batch" def process(self, instance): if not self.is_active(instance.data): return if instance.data.get("renderer") != "vray": # If not V-Ray ignore.. return vray_settings = cmds.ls("vraySettings", type="VRaySettingsNode") assert vray_settings, "Please ensure a VRay Settings Node is present" renderlayer = instance.data['renderlayer'] if not lib.get_attr_in_layer(self.enabled_attr, layer=renderlayer): # If not distributed rendering enabled, ignore.. return # If distributed rendering is enabled but it is *not* set to ignore # during batch mode we invalidate the instance if not lib.get_attr_in_layer(self.ignored_attr, layer=renderlayer): raise PublishValidationError( ("Renderlayer has distributed rendering enabled " "but is not set to ignore in batch mode.")) @classmethod def repair(cls, instance): renderlayer = instance.data.get("renderlayer") with lib.renderlayer(renderlayer): cls.log.debug("Enabling Distributed Rendering " "ignore in batch mode..") cmds.setAttr(cls.ignored_attr, True) ================================================ FILE: openpype/hosts/maya/plugins/publish/validate_vray_referenced_aovs.py ================================================ # -*- coding: utf-8 -*- """Validate if there are AOVs pulled from references.""" import pyblish.api import types from maya import cmds from openpype.pipeline.publish import ( RepairContextAction, OptionalPyblishPluginMixin ) class ValidateVrayReferencedAOVs(pyblish.api.InstancePlugin, OptionalPyblishPluginMixin): """Validate whether the V-Ray Render Elements (AOVs) include references. This will check if there are AOVs pulled from references. If `Vray Use Referenced Aovs` is checked on render instance, u must add those manually to Render Elements as Pype will expect them to be rendered. """ order = pyblish.api.ValidatorOrder label = 'VRay Referenced AOVs' hosts = ['maya'] families = ['renderlayer'] actions = [RepairContextAction] optional = False def process(self, instance): """Plugin main entry point.""" if not self.is_active(instance.data): return if instance.data.get("renderer") != "vray": # If not V-Ray ignore.. return ref_aovs = cmds.ls( type=["VRayRenderElement", "VRayRenderElementSet"], referencedNodes=True) ref_aovs_enabled = ValidateVrayReferencedAOVs.maya_is_true( cmds.getAttr("vraySettings.relements_usereferenced")) if not instance.data.get("vrayUseReferencedAovs"): if ref_aovs_enabled and ref_aovs: self.log.warning(( "Referenced AOVs are enabled in Vray " "Render Settings and are detected in scene, but " "Pype render instance option for referenced AOVs is " "disabled. Those AOVs will be rendered but not published " "by Pype." )) self.log.warning(", ".join(ref_aovs)) else: if not ref_aovs: self.log.warning(( "Use of referenced AOVs enabled but there are none " "in the scene." )) if not ref_aovs_enabled: self.log.error(( "'Use referenced' not enabled in Vray Render Settings." )) raise AssertionError("Invalid render settings") @classmethod def repair(cls, context): """Repair action.""" vray_settings = cmds.ls(type="VRaySettingsNode") if not vray_settings: node = cmds.createNode("VRaySettingsNode") else: node = vray_settings[0] cmds.setAttr("{}.relements_usereferenced".format(node), True) @staticmethod def maya_is_true(attr_val): """Whether a Maya attr evaluates to True. When querying an attribute value from an ambiguous object the Maya API will return a list of values, which need to be properly handled to evaluate properly. Args: attr_val (mixed): Maya attribute to be evaluated as bool. Returns: bool: cast Maya attribute to Pythons boolean value. """ if isinstance(attr_val, bool): return attr_val elif isinstance(attr_val, (list, types.GeneratorType)): return any(attr_val) else: return bool(attr_val) ================================================ FILE: openpype/hosts/maya/plugins/publish/validate_vray_translator_settings.py ================================================ # -*- coding: utf-8 -*- """Validate VRay Translator settings.""" import pyblish.api from openpype.pipeline.publish import ( context_plugin_should_run, RepairContextAction, ValidateContentsOrder, PublishValidationError, OptionalPyblishPluginMixin ) from maya import cmds class ValidateVRayTranslatorEnabled(pyblish.api.ContextPlugin, OptionalPyblishPluginMixin): """Validate VRay Translator settings for extracting vrscenes.""" order = ValidateContentsOrder label = "VRay Translator Settings" families = ["vrayscene_layer"] actions = [RepairContextAction] optional = False def process(self, context): """Plugin entry point.""" if not self.is_active(context.data): return # Workaround bug pyblish-base#250 if not context_plugin_should_run(self, context): return invalid = self.get_invalid(context) if invalid: raise PublishValidationError( message="Found invalid VRay Translator settings", title=self.label ) @classmethod def get_invalid(cls, context): """Get invalid instances.""" invalid = False # Get vraySettings node vray_settings = cmds.ls(type="VRaySettingsNode") if not vray_settings: raise PublishValidationError( "Please ensure a VRay Settings Node is present", title=cls.label ) node = vray_settings[0] if cmds.setAttr("{}.vrscene_render_on".format(node)): cls.log.error( "Render is enabled, for export it should be disabled") invalid = True if not cmds.getAttr("{}.vrscene_on".format(node)): cls.log.error("Export vrscene not enabled") invalid = True for instance in context: if "vrayscene_layer" not in instance.data.get("families"): continue if instance.data.get("vraySceneMultipleFiles"): if not cmds.getAttr("{}.misc_eachFrameInFile".format(node)): cls.log.error("Each Frame in File not enabled") invalid = True else: if cmds.getAttr("{}.misc_eachFrameInFile".format(node)): cls.log.error("Each Frame in File is enabled") invalid = True vrscene_filename = cmds.getAttr("{}.vrscene_filename".format(node)) if vrscene_filename != "vrayscene/ / / ": cls.log.error("Template for file name is wrong") invalid = True return invalid @classmethod def repair(cls, context): """Repair invalid settings.""" vray_settings = cmds.ls(type="VRaySettingsNode") if not vray_settings: node = cmds.createNode("VRaySettingsNode") else: node = vray_settings[0] cmds.setAttr("{}.vrscene_render_on".format(node), False) cmds.setAttr("{}.vrscene_on".format(node), True) for instance in context: if "vrayscene" not in instance.data.get("families"): continue if instance.data.get("vraySceneMultipleFiles"): cmds.setAttr("{}.misc_eachFrameInFile".format(node), True) else: cmds.setAttr("{}.misc_eachFrameInFile".format(node), False) cmds.setAttr("{}.vrscene_filename".format(node), "vrayscene/ / / ", type="string") ================================================ FILE: openpype/hosts/maya/plugins/publish/validate_vrayproxy.py ================================================ import pyblish.api from openpype.pipeline import KnownPublishError from openpype.pipeline.publish import OptionalPyblishPluginMixin class ValidateVrayProxy(pyblish.api.InstancePlugin, OptionalPyblishPluginMixin): order = pyblish.api.ValidatorOrder label = "VRay Proxy Settings" hosts = ["maya"] families = ["vrayproxy"] optional = False def process(self, instance): data = instance.data if not self.is_active(data): return if not data["setMembers"]: raise KnownPublishError( "'%s' is empty! This is a bug" % instance.name ) if data["animation"]: if data["frameEnd"] < data["frameStart"]: raise KnownPublishError( "End frame is smaller than start frame" ) if not data["vrmesh"] and not data["alembic"]: raise KnownPublishError( "Both vrmesh and alembic are off. Needs at least one to" " publish." ) ================================================ FILE: openpype/hosts/maya/plugins/publish/validate_vrayproxy_members.py ================================================ import pyblish.api from maya import cmds import openpype.hosts.maya.api.action from openpype.pipeline.publish import ( PublishValidationError, OptionalPyblishPluginMixin ) class ValidateVrayProxyMembers(pyblish.api.InstancePlugin, OptionalPyblishPluginMixin): """Validate whether the V-Ray Proxy instance has shape members""" order = pyblish.api.ValidatorOrder label = 'VRay Proxy Members' hosts = ['maya'] families = ['vrayproxy'] actions = [openpype.hosts.maya.api.action.SelectInvalidAction] optional = False def process(self, instance): if not self.is_active(instance.data): return invalid = self.get_invalid(instance) if invalid: raise PublishValidationError("'%s' is invalid VRay Proxy for " "export!" % instance.name) @classmethod def get_invalid(cls, instance): shapes = cmds.ls(instance, shapes=True, noIntermediate=True, long=True) if not shapes: cls.log.error("'%s' contains no shapes." % instance.name) # Return the instance itself return [instance.name] ================================================ FILE: openpype/hosts/maya/plugins/publish/validate_xgen.py ================================================ import json import maya.cmds as cmds import xgenm import pyblish.api from openpype.pipeline.publish import PublishValidationError class ValidateXgen(pyblish.api.InstancePlugin): """Validate Xgen data.""" label = "Validate Xgen" order = pyblish.api.ValidatorOrder host = ["maya"] families = ["xgen"] def process(self, instance): set_members = instance.data.get("setMembers") # Only 1 collection/node per instance. if len(set_members) != 1: raise PublishValidationError( "Only one collection per instance is allowed." " Found:\n{}".format(set_members) ) # Only xgen palette node is allowed. node_type = cmds.nodeType(set_members[0]) if node_type != "xgmPalette": raise PublishValidationError( "Only node of type \"xgmPalette\" are allowed. Referred to as" " \"collection\" in the Maya UI." " Node type found: {}".format(node_type) ) # Cant have inactive modifiers in collection cause Xgen will try and # look for them when loading. palette = instance.data["xgmPalette"].replace("|", "") inactive_modifiers = {} for description in instance.data["xgmDescriptions"]: description = description.split("|")[-2] modifier_names = xgenm.fxModules(palette, description) for name in modifier_names: attr = xgenm.getAttr("active", palette, description, name) # Attribute value are lowercase strings of false/true. if attr == "false": try: inactive_modifiers[description].append(name) except KeyError: inactive_modifiers[description] = [name] if inactive_modifiers: raise PublishValidationError( "There are inactive modifiers on the collection. " "Please delete these:\n{}".format( json.dumps(inactive_modifiers, indent=4, sort_keys=True) ) ) # We need a namespace else there will be a naming conflict when # extracting because of stripping namespaces and parenting to world. node_names = [instance.data["xgmPalette"]] node_names.extend(instance.data["xgenConnections"]) non_namespaced_nodes = [n for n in node_names if ":" not in n] if non_namespaced_nodes: raise PublishValidationError( "Could not find namespace on {}. Namespace is required for" " xgen publishing.".format(non_namespaced_nodes) ) ================================================ FILE: openpype/hosts/maya/plugins/publish/validate_yeti_renderscript_callbacks.py ================================================ from maya import cmds import pyblish.api from openpype.pipeline.publish import ( ValidateContentsOrder, OptionalPyblishPluginMixin ) class ValidateYetiRenderScriptCallbacks(pyblish.api.InstancePlugin, OptionalPyblishPluginMixin): """Check if the render script callbacks will be used during the rendering In order to ensure the render tasks are executed properly we need to check if the pre and post render callbacks are actually used. For example: Yeti is not loaded but its callback scripts are still set in the render settings. This will cause an error because Maya tries to find and execute the callbacks. Developer note: The pre and post render callbacks cannot be overridden """ order = ValidateContentsOrder label = "Yeti Render Script Callbacks" hosts = ["maya"] families = ["renderlayer"] optional = False # Settings per renderer callbacks = { "vray": { "pre": "catch(`pgYetiVRayPreRender`)", "post": "catch(`pgYetiVRayPostRender`)" }, "arnold": { "pre": "pgYetiPreRender" } } def process(self, instance): if not self.is_active(instance.data): return invalid = self.get_invalid(instance) if invalid: raise ValueError("Invalid render callbacks found for '%s'!" % instance.name) @classmethod def get_invalid(cls, instance): yeti_loaded = cmds.pluginInfo("pgYetiMaya", query=True, loaded=True) if not yeti_loaded and not cmds.ls(type="pgYetiMaya"): # The yeti plug-in is available and loaded so at # this point we don't really care whether the scene # has any yeti callback set or not since if the callback # is there it wouldn't error and if it weren't then # nothing happens because there are no yeti nodes. cls.log.debug( "Yeti is loaded but no yeti nodes were found. " "Callback validation skipped.." ) return False renderer = instance.data["renderer"] if renderer == "redshift": cls.log.debug("Redshift ignores any pre and post render callbacks") return False callback_lookup = cls.callbacks.get(renderer, {}) if not callback_lookup: cls.log.warning("Renderer '%s' is not supported in this plugin" % renderer) return False pre_mel = cmds.getAttr("defaultRenderGlobals.preMel") or "" post_mel = cmds.getAttr("defaultRenderGlobals.postMel") or "" if pre_mel.strip(): cls.log.debug("Found pre mel: `%s`" % pre_mel) if post_mel.strip(): cls.log.debug("Found post mel: `%s`" % post_mel) # Strip callbacks and turn into a set for quick lookup pre_callbacks = {cmd.strip() for cmd in pre_mel.split(";")} post_callbacks = {cmd.strip() for cmd in post_mel.split(";")} pre_script = callback_lookup.get("pre", "") post_script = callback_lookup.get("post", "") # If Yeti is not loaded invalid = False if not yeti_loaded: if pre_script and pre_script in pre_callbacks: cls.log.error("Found pre render callback '%s' which is not " "uses!" % pre_script) invalid = True if post_script and post_script in post_callbacks: cls.log.error("Found post render callback '%s which is " "not used!" % post_script) invalid = True # If Yeti is loaded else: if pre_script and pre_script not in pre_callbacks: cls.log.error( "Could not find required pre render callback " "`%s`" % pre_script) invalid = True if post_script and post_script not in post_callbacks: cls.log.error( "Could not find required post render callback" " `%s`" % post_script) invalid = True return invalid ================================================ FILE: openpype/hosts/maya/plugins/publish/validate_yeti_rig_cache_state.py ================================================ import pyblish.api import maya.cmds as cmds import openpype.hosts.maya.api.action from openpype.pipeline.publish import ( RepairAction, PublishValidationError, OptionalPyblishPluginMixin ) class ValidateYetiRigCacheState(pyblish.api.InstancePlugin, OptionalPyblishPluginMixin): """Validate the I/O attributes of the node Every pgYetiMaya cache node per instance should have: 1. Input Mode is set to `None` 2. Input Cache File Name is empty """ order = pyblish.api.ValidatorOrder label = "Yeti Rig Cache State" hosts = ["maya"] families = ["yetiRig"] actions = [RepairAction, openpype.hosts.maya.api.action.SelectInvalidAction] optional = False def process(self, instance): if not self.is_active(instance.data): return invalid = self.get_invalid(instance) if invalid: raise PublishValidationError("Nodes have incorrect I/O settings") @classmethod def get_invalid(cls, instance): invalid = [] yeti_nodes = cmds.ls(instance, type="pgYetiMaya") for node in yeti_nodes: # Check reading state state = cmds.getAttr("%s.fileMode" % node) if state == 1: cls.log.error("Node `%s` is set to mode `cache`" % node) invalid.append(node) continue # Check reading state has_cache = cmds.getAttr("%s.cacheFileName" % node) if has_cache: cls.log.error("Node `%s` has a cache file set" % node) invalid.append(node) continue return invalid @classmethod def repair(cls, instance): """Repair all errors""" # Create set to ensure all nodes only pass once invalid = cls.get_invalid(instance) for node in invalid: cmds.setAttr("%s.fileMode" % node, 0) cmds.setAttr("%s.cacheFileName" % node, "", type="string") ================================================ FILE: openpype/hosts/maya/plugins/publish/validate_yeti_rig_input_in_instance.py ================================================ from maya import cmds import pyblish.api import openpype.hosts.maya.api.action from openpype.pipeline.publish import ( ValidateContentsOrder, PublishValidationError, OptionalPyblishPluginMixin ) class ValidateYetiRigInputShapesInInstance(pyblish.api.Validator, OptionalPyblishPluginMixin): """Validate if all input nodes are part of the instance's hierarchy""" order = ValidateContentsOrder hosts = ["maya"] families = ["yetiRig"] label = "Yeti Rig Input Shapes In Instance" actions = [openpype.hosts.maya.api.action.SelectInvalidAction] optional = False def process(self, instance): if not self.is_active(instance.data): return invalid = self.get_invalid(instance) if invalid: raise PublishValidationError("Yeti Rig has invalid input meshes") @classmethod def get_invalid(cls, instance): input_set = next((i for i in instance if i == "input_SET"), None) assert input_set, "Current %s instance has no `input_SET`" % instance # Get all children, we do not care about intermediates input_nodes = cmds.ls(cmds.sets(input_set, query=True), long=True) dag = cmds.ls(input_nodes, dag=True, long=True) shapes = cmds.ls(dag, long=True, shapes=True, noIntermediate=True) # Allow publish without input meshes. if not shapes: cls.log.debug("Found no input meshes for %s, skipping ..." % instance) return [] # check if input node is part of groomRig instance instance_lookup = set(instance[:]) invalid = [s for s in shapes if s not in instance_lookup] return invalid ================================================ FILE: openpype/hosts/maya/plugins/publish/validate_yeti_rig_settings.py ================================================ import pyblish.api from openpype.pipeline.publish import ( PublishValidationError, OptionalPyblishPluginMixin ) class ValidateYetiRigSettings(pyblish.api.InstancePlugin, OptionalPyblishPluginMixin): """Validate Yeti Rig Settings have collected input connections. The input connections are collected for the nodes in the `input_SET`. When no input connections are found a warning is logged but it is allowed to pass validation. """ order = pyblish.api.ValidatorOrder label = "Yeti Rig Settings" families = ["yetiRig"] optional = False def process(self, instance): if not self.is_active(instance.data): return invalid = self.get_invalid(instance) if invalid: raise PublishValidationError( ("Detected invalid Yeti Rig data. (See log) " "Tip: Save the scene")) @classmethod def get_invalid(cls, instance): rigsettings = instance.data.get("rigsettings", None) if rigsettings is None: cls.log.error("MAJOR ERROR: No rig settings found!") return True # Get inputs inputs = rigsettings.get("inputs", []) if not inputs: # Empty rig settings dictionary cls.log.warning("No rig inputs found. This can happen when " "the rig has no inputs from outside the rig.") return False for input in inputs: source_id = input["sourceID"] if source_id is None: cls.log.error("Discovered source with 'None' as ID, please " "check if the input shape has a cbId") return True destination_id = input["destinationID"] if destination_id is None: cls.log.error("Discovered None as destination ID value") return True return False ================================================ FILE: openpype/hosts/maya/startup/userSetup.py ================================================ import os from openpype.settings import get_project_settings from openpype.pipeline import install_host, get_current_project_name from openpype.hosts.maya.api import MayaHost from maya import cmds host = MayaHost() install_host(host) print("Starting OpenPype usersetup...") project_name = get_current_project_name() settings = get_project_settings(project_name) # Loading plugins explicitly. explicit_plugins_loading = settings["maya"]["explicit_plugins_loading"] if explicit_plugins_loading["enabled"]: def _explicit_load_plugins(): for plugin in explicit_plugins_loading["plugins_to_load"]: if plugin["enabled"]: print("Loading plug-in: " + plugin["name"]) try: cmds.loadPlugin(plugin["name"], quiet=True) except RuntimeError as e: print(e) # We need to load plugins deferred as loading them directly does not work # correctly due to Maya's initialization. cmds.evalDeferred( _explicit_load_plugins, lowestPriority=True ) # Open Workfile Post Initialization. key = "OPENPYPE_OPEN_WORKFILE_POST_INITIALIZATION" if bool(int(os.environ.get(key, "0"))): def _log_and_open(): path = os.environ["AVALON_LAST_WORKFILE"] print("Opening \"{}\"".format(path)) cmds.file(path, open=True, force=True) cmds.evalDeferred( _log_and_open, lowestPriority=True ) print("Finished OpenPype usersetup.") ================================================ FILE: openpype/hosts/maya/tools/__init__.py ================================================ from openpype.tools.utils.host_tools import qt_app_context class MayaToolsSingleton: _look_assigner = None def get_look_assigner_tool(parent): """Create, cache and return look assigner tool window.""" if MayaToolsSingleton._look_assigner is None: from .mayalookassigner import MayaLookAssignerWindow mayalookassigner_window = MayaLookAssignerWindow(parent) MayaToolsSingleton._look_assigner = mayalookassigner_window return MayaToolsSingleton._look_assigner def show_look_assigner(parent=None): """Look manager is Maya specific tool for look management.""" with qt_app_context(): look_assigner_tool = get_look_assigner_tool(parent) look_assigner_tool.show() # Pull window to the front. look_assigner_tool.raise_() look_assigner_tool.activateWindow() look_assigner_tool.showNormal() ================================================ FILE: openpype/hosts/maya/tools/mayalookassigner/LICENSE ================================================ MIT License Copyright (c) 2017 Colorbleed Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: openpype/hosts/maya/tools/mayalookassigner/__init__.py ================================================ from .app import ( MayaLookAssignerWindow, show ) __all__ = [ "MayaLookAssignerWindow", "show"] ================================================ FILE: openpype/hosts/maya/tools/mayalookassigner/alembic.py ================================================ # -*- coding: utf-8 -*- """Tools for loading looks to vray proxies.""" import os from collections import defaultdict import logging import six import alembic.Abc log = logging.getLogger(__name__) def get_alembic_paths_by_property(filename, attr, verbose=False): # type: (str, str, bool) -> dict """Return attribute value per objects in the Alembic file. Reads an Alembic archive hierarchy and retrieves the value from the `attr` properties on the objects. Args: filename (str): Full path to Alembic archive to read. attr (str): Id attribute. verbose (bool): Whether to verbosely log missing attributes. Returns: dict: Mapping of node full path with its id """ # Normalize alembic path filename = os.path.normpath(filename) filename = filename.replace("\\", "/") filename = str(filename) # path must be string try: archive = alembic.Abc.IArchive(filename) except RuntimeError: # invalid alembic file - probably vrmesh log.warning("{} is not an alembic file".format(filename)) return {} root = archive.getTop() iterator = list(root.children) obj_ids = {} for obj in iterator: name = obj.getFullName() # include children for coming iterations iterator.extend(obj.children) props = obj.getProperties() if props.getNumProperties() == 0: # Skip those without properties, e.g. '/materials' in a gpuCache continue # THe custom attribute is under the properties' first container under # the ".arbGeomParams" prop = props.getProperty(0) # get base property _property = None try: geo_params = prop.getProperty('.arbGeomParams') _property = geo_params.getProperty(attr) except KeyError: if verbose: log.debug("Missing attr on: {0}".format(name)) continue if not _property.isConstant(): log.warning("Id not constant on: {0}".format(name)) # Get first value sample value = _property.getValue()[0] obj_ids[name] = value return obj_ids def get_alembic_ids_cache(path): # type: (str) -> dict """Build a id to node mapping in Alembic file. Nodes without IDs are ignored. Returns: dict: Mapping of id to nodes in the Alembic. """ node_ids = get_alembic_paths_by_property(path, attr="cbId") id_nodes = defaultdict(list) for node, _id in six.iteritems(node_ids): id_nodes[_id].append(node) return dict(six.iteritems(id_nodes)) ================================================ FILE: openpype/hosts/maya/tools/mayalookassigner/app.py ================================================ import sys import time import logging from qtpy import QtWidgets, QtCore from openpype import style from openpype.client import get_last_version_by_subset_id from openpype.pipeline import get_current_project_name from openpype.tools.utils.lib import qt_app_context from openpype.hosts.maya.api.lib import ( assign_look_by_version, get_main_window ) from maya import cmds # old api for MFileIO import maya.OpenMaya import maya.api.OpenMaya as om from .widgets import ( AssetOutliner, LookOutliner ) from .commands import ( get_workfile, remove_unused_looks ) from .vray_proxies import vrayproxy_assign_look from . import arnold_standin module = sys.modules[__name__] module.window = None class MayaLookAssignerWindow(QtWidgets.QWidget): def __init__(self, parent=None): super(MayaLookAssignerWindow, self).__init__(parent=parent) self.log = logging.getLogger(__name__) # Store callback references self._callbacks = [] self._connections_set_up = False filename = get_workfile() self.setObjectName("lookManager") self.setWindowTitle("Look Manager 1.4.0 - [{}]".format(filename)) self.setWindowFlags(QtCore.Qt.Window) self.setParent(parent) self.resize(750, 500) self.setup_ui() # Force refresh check on initialization self._on_renderlayer_switch() def setup_ui(self): """Build the UI""" main_splitter = QtWidgets.QSplitter(self) # Assets (left) asset_outliner = AssetOutliner(main_splitter) # Looks (right) looks_widget = QtWidgets.QWidget(main_splitter) look_outliner = LookOutliner(looks_widget) # Database look overview assign_selected = QtWidgets.QCheckBox( "Assign to selected only", looks_widget ) assign_selected.setToolTip("Whether to assign only to selected nodes " "or to the full asset") remove_unused_btn = QtWidgets.QPushButton( "Remove Unused Looks", looks_widget ) looks_layout = QtWidgets.QVBoxLayout(looks_widget) looks_layout.addWidget(look_outliner) looks_layout.addWidget(assign_selected) looks_layout.addWidget(remove_unused_btn) main_splitter.addWidget(asset_outliner) main_splitter.addWidget(looks_widget) main_splitter.setSizes([350, 200]) # Footer status = QtWidgets.QStatusBar(self) status.setSizeGripEnabled(False) status.setFixedHeight(25) warn_layer = QtWidgets.QLabel( "Current Layer is not defaultRenderLayer", self ) warn_layer.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter) warn_layer.setStyleSheet("color: #DD5555; font-weight: bold;") warn_layer.setFixedHeight(25) footer = QtWidgets.QHBoxLayout() footer.setContentsMargins(0, 0, 0, 0) footer.addWidget(status) footer.addWidget(warn_layer) # Build up widgets main_layout = QtWidgets.QVBoxLayout(self) main_layout.setSpacing(0) main_layout.addWidget(main_splitter) main_layout.addLayout(footer) # Set column width asset_outliner.view.setColumnWidth(0, 200) look_outliner.view.setColumnWidth(0, 150) asset_outliner.selection_changed.connect( self.on_asset_selection_changed) asset_outliner.refreshed.connect( lambda: self.echo("Loaded assets..") ) look_outliner.menu_apply_action.connect(self.on_process_selected) remove_unused_btn.clicked.connect(remove_unused_looks) # Open widgets self.asset_outliner = asset_outliner self.look_outliner = look_outliner self.status = status self.warn_layer = warn_layer # Buttons self.remove_unused = remove_unused_btn self.assign_selected = assign_selected self._first_show = True def setup_connections(self): """Connect interactive widgets with actions""" if self._connections_set_up: return # Maya renderlayer switch callback callback = om.MEventMessage.addEventCallback( "renderLayerManagerChange", self._on_renderlayer_switch ) self._callbacks.append(callback) self._connections_set_up = True def remove_connection(self): # Delete callbacks for callback in self._callbacks: om.MMessage.removeCallback(callback) self._callbacks = [] self._connections_set_up = False def showEvent(self, event): self.setup_connections() super(MayaLookAssignerWindow, self).showEvent(event) if self._first_show: self._first_show = False self.setStyleSheet(style.load_stylesheet()) def closeEvent(self, event): self.remove_connection() super(MayaLookAssignerWindow, self).closeEvent(event) def _on_renderlayer_switch(self, *args): """Callback that updates on Maya renderlayer switch""" if maya.OpenMaya.MFileIO.isNewingFile(): # Don't perform a check during file open or file new as # the renderlayers will not be in a valid state yet. return layer = cmds.editRenderLayerGlobals(query=True, currentRenderLayer=True) if layer != "defaultRenderLayer": self.warn_layer.show() else: self.warn_layer.hide() def echo(self, message): self.status.showMessage(message, 1500) def refresh(self): """Refresh the content""" # Get all containers and information self.asset_outliner.clear() found_items = self.asset_outliner.get_all_assets() if not found_items: self.look_outliner.clear() def on_asset_selection_changed(self): """Get selected items from asset loader and fill look outliner""" items = self.asset_outliner.get_selected_items() self.look_outliner.clear() self.look_outliner.add_items(items) def on_process_selected(self): """Process all selected looks for the selected assets""" assets = self.asset_outliner.get_selected_items() assert assets, "No asset selected" # Collect the looks we want to apply (by name) look_items = self.look_outliner.get_selected_items() looks = {look["subset"] for look in look_items} selection = self.assign_selected.isChecked() asset_nodes = self.asset_outliner.get_nodes(selection=selection) project_name = get_current_project_name() start = time.time() for i, (asset, item) in enumerate(asset_nodes.items()): # Label prefix prefix = "({}/{})".format(i + 1, len(asset_nodes)) # Assign the first matching look relevant for this asset # (since assigning multiple to the same nodes makes no sense) assign_look = next((subset for subset in item["looks"] if subset["name"] in looks), None) if not assign_look: self.echo( "{} No matching selected look for {}".format(prefix, asset) ) continue # Get the latest version of this asset's look subset version = get_last_version_by_subset_id( project_name, assign_look["_id"], fields=["_id"] ) subset_name = assign_look["name"] self.echo("{} Assigning {} to {}\t".format( prefix, subset_name, asset )) nodes = item["nodes"] # Assign Vray Proxy look. if cmds.pluginInfo('vrayformaya', query=True, loaded=True): self.echo("Getting vray proxy nodes ...") vray_proxies = set(cmds.ls(type="VRayProxy", long=True)) for vp in vray_proxies: if vp in nodes: vrayproxy_assign_look(vp, subset_name) nodes = list(set(nodes).difference(vray_proxies)) else: self.echo( "Could not assign to VRayProxy because vrayformaya plugin " "is not loaded." ) # Assign Arnold Standin look. if cmds.pluginInfo("mtoa", query=True, loaded=True): arnold_standins = set(cmds.ls(type="aiStandIn", long=True)) for standin in arnold_standins: if standin in nodes: arnold_standin.assign_look(standin, subset_name) nodes = list(set(nodes).difference(arnold_standins)) else: self.echo( "Could not assign to aiStandIn because mtoa plugin is not " "loaded." ) # Assign look if nodes: assign_look_by_version(nodes, version_id=version["_id"]) end = time.time() self.echo("Finished assigning.. ({0:.3f}s)".format(end - start)) def show(): """Display Loader GUI Arguments: debug (bool, optional): Run loader in debug-mode, defaults to False """ try: module.window.close() del module.window except (RuntimeError, AttributeError): pass # Get Maya main window mainwindow = get_main_window() with qt_app_context(): window = MayaLookAssignerWindow(parent=mainwindow) window.show() module.window = window ================================================ FILE: openpype/hosts/maya/tools/mayalookassigner/arnold_standin.py ================================================ import os import json from collections import defaultdict import logging from maya import cmds from openpype.pipeline import legacy_io from openpype.client import get_last_version_by_subset_name from openpype.hosts.maya import api from . import lib from .alembic import get_alembic_ids_cache from .usd import is_usd_lib_supported, get_usd_ids_cache log = logging.getLogger(__name__) ATTRIBUTE_MAPPING = { "primaryVisibility": "visibility", # Camera "castsShadows": "visibility", # Shadow "receiveShadows": "receive_shadows", "aiSelfShadows": "self_shadows", "aiOpaque": "opaque", "aiMatte": "matte", "aiVisibleInDiffuseTransmission": "visibility", "aiVisibleInSpecularTransmission": "visibility", "aiVisibleInVolume": "visibility", "aiVisibleInDiffuseReflection": "visibility", "aiVisibleInSpecularReflection": "visibility", "aiSubdivUvSmoothing": "subdiv_uv_smoothing", "aiDispHeight": "disp_height", "aiDispPadding": "disp_padding", "aiDispZeroValue": "disp_zero_value", "aiStepSize": "step_size", "aiVolumePadding": "volume_padding", "aiSubdivType": "subdiv_type", "aiSubdivIterations": "subdiv_iterations" } def calculate_visibility_mask(attributes): # https://arnoldsupport.com/2018/11/21/backdoor-setting-visibility/ mapping = { "primaryVisibility": 1, # Camera "castsShadows": 2, # Shadow "aiVisibleInDiffuseTransmission": 4, "aiVisibleInSpecularTransmission": 8, "aiVisibleInVolume": 16, "aiVisibleInDiffuseReflection": 32, "aiVisibleInSpecularReflection": 64 } mask = 255 for attr, value in mapping.items(): if attributes.get(attr, True): continue mask -= value return mask def get_nodes_by_id(standin): """Get node id from aiStandIn via json sidecar. Args: standin (string): aiStandIn node. Returns: (dict): Dictionary with node full name/path and id. """ path = cmds.getAttr(standin + ".dso") if path.endswith(".abc"): # Support alembic files directly return get_alembic_ids_cache(path) elif ( is_usd_lib_supported and any(path.endswith(ext) for ext in [".usd", ".usda", ".usdc"]) ): # Support usd files directly return get_usd_ids_cache(path) json_path = None for f in os.listdir(os.path.dirname(path)): if f.endswith(".json"): json_path = os.path.join(os.path.dirname(path), f) break if not json_path: log.warning("Could not find json file for {}.".format(standin)) return {} with open(json_path, "r") as f: return json.load(f) def shading_engine_assignments(shading_engine, attribute, nodes, assignments): """Full assignments with shader or disp_map. Args: shading_engine (string): Shading engine for material. attribute (string): "surfaceShader" or "displacementShader" nodes: (list): Nodes paths relative to aiStandIn. assignments (dict): Assignments by nodes. """ shader_inputs = cmds.listConnections( shading_engine + "." + attribute, source=True ) if not shader_inputs: log.info( "Shading engine \"{}\" missing input \"{}\"".format( shading_engine, attribute ) ) return # Strip off component assignments for i, node in enumerate(nodes): if "." in node: log.warning( "Converting face assignment to full object assignment. This " "conversion can be lossy: {}".format(node) ) nodes[i] = node.split(".")[0] shader_type = "shader" if attribute == "surfaceShader" else "disp_map" assignment = "{}='{}'".format(shader_type, shader_inputs[0]) for node in nodes: assignments[node].append(assignment) def assign_look(standin, subset): log.info("Assigning {} to {}.".format(subset, standin)) nodes_by_id = get_nodes_by_id(standin) # Group by asset id so we run over the look per asset node_ids_by_asset_id = defaultdict(set) for node_id in nodes_by_id: asset_id = node_id.split(":", 1)[0] node_ids_by_asset_id[asset_id].add(node_id) project_name = legacy_io.active_project() for asset_id, node_ids in node_ids_by_asset_id.items(): # Get latest look version version = get_last_version_by_subset_name( project_name, subset_name=subset, asset_id=asset_id, fields=["_id"] ) if not version: log.info("Didn't find last version for subset name {}".format( subset )) continue relationships = lib.get_look_relationships(version["_id"]) shader_nodes, container_node = lib.load_look(version["_id"]) namespace = shader_nodes[0].split(":")[0] # Get only the node ids and paths related to this asset # And get the shader edits the look supplies asset_nodes_by_id = { node_id: nodes_by_id[node_id] for node_id in node_ids } edits = list( api.lib.iter_shader_edits( relationships, shader_nodes, asset_nodes_by_id ) ) # Create assignments node_assignments = {} for edit in edits: for node in edit["nodes"]: if node not in node_assignments: node_assignments[node] = [] if edit["action"] == "assign": if not cmds.ls(edit["shader"], type="shadingEngine"): log.info("Skipping non-shader: %s" % edit["shader"]) continue shading_engine_assignments( shading_engine=edit["shader"], attribute="surfaceShader", nodes=edit["nodes"], assignments=node_assignments ) shading_engine_assignments( shading_engine=edit["shader"], attribute="displacementShader", nodes=edit["nodes"], assignments=node_assignments ) if edit["action"] == "setattr": visibility = False for attr, value in edit["attributes"].items(): if attr not in ATTRIBUTE_MAPPING: log.warning( "Skipping setting attribute {} on {} because it is" " not recognized.".format(attr, edit["nodes"]) ) continue if isinstance(value, str): value = "'{}'".format(value) if ATTRIBUTE_MAPPING[attr] == "visibility": visibility = True continue assignment = "{}={}".format(ATTRIBUTE_MAPPING[attr], value) for node in edit["nodes"]: node_assignments[node].append(assignment) if visibility: mask = calculate_visibility_mask(edit["attributes"]) assignment = "visibility={}".format(mask) for node in edit["nodes"]: node_assignments[node].append(assignment) # Assign shader # Clear all current shader assignments plug = standin + ".operators" num = cmds.getAttr(plug, size=True) for i in reversed(range(num)): cmds.removeMultiInstance("{}[{}]".format(plug, i), b=True) # Create new assignment overrides index = 0 for node, assignments in node_assignments.items(): if not assignments: continue with api.lib.maintained_selection(): operator = cmds.createNode("aiSetParameter") operator = cmds.rename(operator, namespace + ":" + operator) cmds.setAttr(operator + ".selection", node, type="string") for i, assignment in enumerate(assignments): cmds.setAttr( "{}.assignment[{}]".format(operator, i), assignment, type="string" ) cmds.connectAttr( operator + ".out", "{}[{}]".format(plug, index) ) index += 1 cmds.sets(operator, edit=True, addElement=container_node) ================================================ FILE: openpype/hosts/maya/tools/mayalookassigner/commands.py ================================================ import os import logging from collections import defaultdict import maya.cmds as cmds from openpype.client import get_assets, get_asset_name_identifier from openpype.pipeline import ( remove_container, registered_host, get_current_project_name, ) from openpype.hosts.maya.api import lib from .vray_proxies import get_alembic_ids_cache from . import arnold_standin log = logging.getLogger(__name__) def get_workfile(): path = cmds.file(query=True, sceneName=True) or "untitled" return os.path.basename(path) def get_workfolder(): return os.path.dirname(cmds.file(query=True, sceneName=True)) def select(nodes): cmds.select(nodes) def get_namespace_from_node(node): """Get the namespace from the given node Args: node (str): name of the node Returns: namespace (str) """ parts = node.rsplit("|", 1)[-1].rsplit(":", 1) return parts[0] if len(parts) > 1 else u":" def get_selected_nodes(): """Get information from current selection""" selection = cmds.ls(selection=True, long=True) hierarchy = lib.get_all_children(selection) return list(set(selection + hierarchy)) def get_all_asset_nodes(): """Get all assets from the scene, container based Returns: list: list of dictionaries """ return cmds.ls(dag=True, noIntermediate=True, long=True) def create_asset_id_hash(nodes): """Create a hash based on cbId attribute value Args: nodes (list): a list of nodes Returns: dict """ node_id_hash = defaultdict(list) for node in nodes: # iterate over content of reference node if cmds.nodeType(node) == "reference": ref_hashes = create_asset_id_hash( list(set(cmds.referenceQuery(node, nodes=True, dp=True)))) for asset_id, ref_nodes in ref_hashes.items(): node_id_hash[asset_id] += ref_nodes elif cmds.pluginInfo('vrayformaya', query=True, loaded=True) and cmds.nodeType( node) == "VRayProxy": path = cmds.getAttr("{}.fileName".format(node)) ids = get_alembic_ids_cache(path) for k, _ in ids.items(): id = k.split(":")[0] node_id_hash[id].append(node) elif cmds.nodeType(node) == "aiStandIn": for id, _ in arnold_standin.get_nodes_by_id(node).items(): id = id.split(":")[0] node_id_hash[id].append(node) else: value = lib.get_id(node) if value is None: continue asset_id = value.split(":")[0] node_id_hash[asset_id].append(node) return dict(node_id_hash) def create_items_from_nodes(nodes): """Create an item for the view based the container and content of it It fetches the look document based on the asset ID found in the content. The item will contain all important information for the tool to work. If there is an asset ID which is not registered in the project's collection it will log a warning message. Args: nodes (list): list of maya nodes Returns: list of dicts """ asset_view_items = [] id_hashes = create_asset_id_hash(nodes) if not id_hashes: log.warning("No id hashes") return asset_view_items project_name = get_current_project_name() asset_ids = set(id_hashes.keys()) fields = {"_id", "name", "data.parents"} asset_docs = get_assets(project_name, asset_ids, fields=fields) asset_docs_by_id = { str(asset_doc["_id"]): asset_doc for asset_doc in asset_docs } for asset_id, id_nodes in id_hashes.items(): asset_doc = asset_docs_by_id.get(asset_id) # Skip if asset id is not found if not asset_doc: log.warning( "Id found on {num} nodes for which no asset is found database," " skipping '{asset_id}'".format( num=len(nodes), asset_id=asset_id ) ) continue # Collect available look subsets for this asset looks = lib.list_looks(project_name, asset_doc["_id"]) # Collect namespaces the asset is found in namespaces = set() for node in id_nodes: namespace = get_namespace_from_node(node) namespaces.add(namespace) label = get_asset_name_identifier(asset_doc) asset_view_items.append({ "label": label, "asset": asset_doc, "looks": looks, "namespaces": namespaces }) return asset_view_items def remove_unused_looks(): """Removes all loaded looks for which none of the shaders are used. This will cleanup all loaded "LookLoader" containers that are unused in the current scene. """ host = registered_host() unused = [] for container in host.ls(): if container['loader'] == "LookLoader": members = lib.get_container_members(container['objectName']) look_sets = cmds.ls(members, type="objectSet") for look_set in look_sets: # If the set is used than we consider this look *in use* if cmds.sets(look_set, query=True): break else: unused.append(container) for container in unused: log.info("Removing unused look container: %s", container['objectName']) remove_container(container) log.info("Finished removing unused looks. (see log for details)") ================================================ FILE: openpype/hosts/maya/tools/mayalookassigner/lib.py ================================================ import json import logging from openpype.pipeline import ( legacy_io, get_representation_path, registered_host, discover_loader_plugins, loaders_from_representation, load_container ) from openpype.client import get_representation_by_name from openpype.hosts.maya.api import lib log = logging.getLogger(__name__) def get_look_relationships(version_id): # type: (str) -> dict """Get relations for the look. Args: version_id (str): Parent version Id. Returns: dict: Dictionary of relations. """ project_name = legacy_io.active_project() json_representation = get_representation_by_name( project_name, representation_name="json", version_id=version_id ) # Load relationships shader_relation = get_representation_path(json_representation) with open(shader_relation, "r") as f: relationships = json.load(f) return relationships def load_look(version_id): # type: (str) -> list """Load look from version. Get look from version and invoke Loader for it. Args: version_id (str): Version ID Returns: list of shader nodes. """ project_name = legacy_io.active_project() # Get representations of shader file and relationships look_representation = get_representation_by_name( project_name, representation_name="ma", version_id=version_id ) # See if representation is already loaded, if so reuse it. host = registered_host() representation_id = str(look_representation['_id']) for container in host.ls(): if (container['loader'] == "LookLoader" and container['representation'] == representation_id): log.info("Reusing loaded look ...") container_node = container['objectName'] break else: log.info("Using look for the first time ...") # Load file all_loaders = discover_loader_plugins() loaders = loaders_from_representation(all_loaders, representation_id) loader = next( (i for i in loaders if i.__name__ == "LookLoader"), None) if loader is None: raise RuntimeError("Could not find LookLoader, this is a bug") # Reference the look file with lib.maintained_selection(): container_node = load_container(loader, look_representation)[0] return lib.get_container_members(container_node), container_node ================================================ FILE: openpype/hosts/maya/tools/mayalookassigner/models.py ================================================ from collections import defaultdict from qtpy import QtCore import qtawesome from openpype.tools.utils import models from openpype.style import get_default_entity_icon_color class AssetModel(models.TreeModel): Columns = ["label"] def __init__(self, *args, **kwargs): super(AssetModel, self).__init__(*args, **kwargs) self._icon_color = get_default_entity_icon_color() def add_items(self, items): """ Add items to model with needed data Args: items(list): collection of item data Returns: None """ self.beginResetModel() # Add the items sorted by label sorter = lambda x: x["label"] for item in sorted(items, key=sorter): asset_item = models.Item() asset_item.update(item) asset_item["icon"] = "folder" # Add namespace children namespaces = item["namespaces"] for namespace in sorted(namespaces): child = models.Item() child.update(item) child.update({ "label": (namespace if namespace != ":" else "(no namespace)"), "namespace": namespace, "looks": item["looks"], "icon": "folder-o" }) asset_item.add_child(child) self.add_child(asset_item) self.endResetModel() def data(self, index, role): if not index.isValid(): return if role == models.TreeModel.ItemRole: node = index.internalPointer() return node # Add icon if role == QtCore.Qt.DecorationRole: if index.column() == 0: node = index.internalPointer() icon = node.get("icon") if icon: return qtawesome.icon( "fa.{0}".format(icon), color=self._icon_color ) return super(AssetModel, self).data(index, role) class LookModel(models.TreeModel): """Model displaying a list of looks and matches for assets""" Columns = ["label", "match"] def add_items(self, items): """Add items to model with needed data An item exists of: { "subset": 'name of subset', "asset": asset_document } Args: items(list): collection of item data Returns: None """ self.beginResetModel() # Collect the assets per look name (from the items of the AssetModel) look_subsets = defaultdict(list) for asset_item in items: asset = asset_item["asset"] for look in asset_item["looks"]: look_subsets[look["name"]].append(asset) for subset in sorted(look_subsets.keys()): assets = look_subsets[subset] # Define nice label without "look" prefix for readability label = subset if not subset.startswith("look") else subset[4:] item_node = models.Item() item_node["label"] = label item_node["subset"] = subset # Amount of matching assets for this look item_node["match"] = len(assets) # Store the assets that have this subset available item_node["assets"] = assets self.add_child(item_node) self.endResetModel() ================================================ FILE: openpype/hosts/maya/tools/mayalookassigner/usd.py ================================================ from collections import defaultdict try: from pxr import Usd is_usd_lib_supported = True except ImportError: is_usd_lib_supported = False def get_usd_ids_cache(path): # type: (str) -> dict """Build a id to node mapping in a USD file. Nodes without IDs are ignored. Returns: dict: Mapping of id to nodes in the USD file. """ if not is_usd_lib_supported: raise RuntimeError("No pxr.Usd python library available.") stage = Usd.Stage.Open(path) ids = {} for prim in stage.Traverse(): attr = prim.GetAttribute("userProperties:cbId") if not attr.IsValid(): continue value = attr.Get() if not value: continue path = str(prim.GetPath()) ids[path] = value cache = defaultdict(list) for path, value in ids.items(): cache[value].append(path) return dict(cache) ================================================ FILE: openpype/hosts/maya/tools/mayalookassigner/views.py ================================================ from qtpy import QtWidgets, QtCore class View(QtWidgets.QTreeView): data_changed = QtCore.Signal() def __init__(self, parent=None): super(View, self).__init__(parent=parent) # view settings self.setAlternatingRowColors(False) self.setSortingEnabled(True) self.setSelectionMode(QtWidgets.QAbstractItemView.ExtendedSelection) self.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) def get_indices(self): """Get the selected rows""" selection_model = self.selectionModel() return selection_model.selectedRows() def extend_to_children(self, indices): """Extend the indices to the children indices. Top-level indices are extended to its children indices. Sub-items are kept as is. :param indices: The indices to extend. :type indices: list :return: The children indices :rtype: list """ subitems = set() for i in indices: valid_parent = i.parent().isValid() if valid_parent and i not in subitems: subitems.add(i) else: # is top level node model = i.model() rows = model.rowCount(parent=i) for row in range(rows): child = model.index(row, 0, parent=i) subitems.add(child) return list(subitems) ================================================ FILE: openpype/hosts/maya/tools/mayalookassigner/vray_proxies.py ================================================ # -*- coding: utf-8 -*- """Tools for loading looks to vray proxies.""" from collections import defaultdict import logging from maya import cmds from openpype.client import get_last_version_by_subset_name from openpype.pipeline import get_current_project_name import openpype.hosts.maya.lib as maya_lib from . import lib from .alembic import get_alembic_ids_cache log = logging.getLogger(__name__) def assign_vrayproxy_shaders(vrayproxy, assignments): # type: (str, dict) -> None """Assign shaders to content of Vray Proxy. This will create shader overrides on Vray Proxy to assign shaders to its content. Todo: Allow to optimize and assign a single shader to multiple shapes at once or maybe even set it to the highest available path? Args: vrayproxy (str): Name of Vray Proxy assignments (dict): Mapping of shader assignments. Returns: None """ # Clear all current shader assignments plug = vrayproxy + ".shaders" num = cmds.getAttr(plug, size=True) for i in reversed(range(num)): cmds.removeMultiInstance("{}[{}]".format(plug, i), b=True) # Create new assignment overrides index = 0 for material, paths in assignments.items(): for path in paths: plug = "{}.shaders[{}]".format(vrayproxy, index) cmds.setAttr(plug + ".shadersNames", path, type="string") cmds.connectAttr(material + ".outColor", plug + ".shadersConnections", force=True) index += 1 def vrayproxy_assign_look(vrayproxy, subset="lookDefault"): # type: (str, str) -> None """Assign look to vray proxy. Args: vrayproxy (str): Name of vrayproxy to apply look to. subset (str): Name of look subset. Returns: None """ path = cmds.getAttr(vrayproxy + ".fileName") nodes_by_id = get_alembic_ids_cache(path) if not nodes_by_id: log.warning("Alembic file has no cbId attributes: %s" % path) return # Group by asset id so we run over the look per asset node_ids_by_asset_id = defaultdict(set) for node_id in nodes_by_id: asset_id = node_id.split(":", 1)[0] node_ids_by_asset_id[asset_id].add(node_id) project_name = get_current_project_name() for asset_id, node_ids in node_ids_by_asset_id.items(): # Get latest look version version = get_last_version_by_subset_name( project_name, subset_name=subset, asset_id=asset_id, fields=["_id"] ) if not version: print("Didn't find last version for subset name {}".format( subset )) continue relationships = lib.get_look_relationships(version["_id"]) shadernodes, _ = lib.load_look(version["_id"]) # Get only the node ids and paths related to this asset # And get the shader edits the look supplies asset_nodes_by_id = { node_id: nodes_by_id[node_id] for node_id in node_ids } edits = list( maya_lib.iter_shader_edits( relationships, shadernodes, asset_nodes_by_id ) ) # Create assignments assignments = {} for edit in edits: if edit["action"] == "assign": nodes = edit["nodes"] shader = edit["shader"] if not cmds.ls(shader, type="shadingEngine"): print("Skipping non-shader: %s" % shader) continue inputs = cmds.listConnections( shader + ".surfaceShader", source=True) if not inputs: print("Shading engine missing material: %s" % shader) # Strip off component assignments for i, node in enumerate(nodes): if "." in node: log.warning( ("Converting face assignment to full object " "assignment. This conversion can be lossy: " "{}").format(node)) nodes[i] = node.split(".")[0] material = inputs[0] assignments[material] = nodes assign_vrayproxy_shaders(vrayproxy, assignments) ================================================ FILE: openpype/hosts/maya/tools/mayalookassigner/widgets.py ================================================ import logging from collections import defaultdict from qtpy import QtWidgets, QtCore from openpype.client import get_asset_name_identifier from openpype.tools.utils.models import TreeModel from openpype.tools.utils.lib import ( preserve_expanded_rows, preserve_selection, ) from .models import ( AssetModel, LookModel ) from . import commands from .views import View from maya import cmds class AssetOutliner(QtWidgets.QWidget): refreshed = QtCore.Signal() selection_changed = QtCore.Signal() def __init__(self, parent=None): super(AssetOutliner, self).__init__(parent) title = QtWidgets.QLabel("Assets", self) title.setAlignment(QtCore.Qt.AlignCenter) title.setStyleSheet("font-weight: bold; font-size: 12px") model = AssetModel() view = View(self) view.setModel(model) view.customContextMenuRequested.connect(self.right_mouse_menu) view.setSortingEnabled(False) view.setHeaderHidden(True) view.setIndentation(10) from_all_asset_btn = QtWidgets.QPushButton( "Get All Assets", self ) from_selection_btn = QtWidgets.QPushButton( "Get Assets From Selection", self ) layout = QtWidgets.QVBoxLayout(self) layout.addWidget(title) layout.addWidget(from_all_asset_btn) layout.addWidget(from_selection_btn) layout.addWidget(view) # Build connections from_selection_btn.clicked.connect(self.get_selected_assets) from_all_asset_btn.clicked.connect(self.get_all_assets) selection_model = view.selectionModel() selection_model.selectionChanged.connect(self.selection_changed) self.view = view self.model = model self.log = logging.getLogger(__name__) def clear(self): self.model.clear() # fix looks remaining visible when no items present after "refresh" # todo: figure out why this workaround is needed. self.selection_changed.emit() def add_items(self, items): """Add new items to the outliner""" self.model.add_items(items) self.refreshed.emit() def get_selected_items(self): """Get current selected items from view Returns: list: list of dictionaries """ selection_model = self.view.selectionModel() return [row.data(TreeModel.ItemRole) for row in selection_model.selectedRows(0)] def get_all_assets(self): """Add all items from the current scene""" with preserve_expanded_rows(self.view): with preserve_selection(self.view): self.clear() nodes = commands.get_all_asset_nodes() items = commands.create_items_from_nodes(nodes) self.add_items(items) return len(items) > 0 def get_selected_assets(self): """Add all selected items from the current scene""" with preserve_expanded_rows(self.view): with preserve_selection(self.view): self.clear() nodes = commands.get_selected_nodes() items = commands.create_items_from_nodes(nodes) self.add_items(items) def get_nodes(self, selection=False): """Find the nodes in the current scene per asset.""" items = self.get_selected_items() # Collect all nodes by hash (optimization) if not selection: nodes = cmds.ls(dag=True, long=True) else: nodes = commands.get_selected_nodes() id_nodes = commands.create_asset_id_hash(nodes) # Collect the asset item entries per asset # and collect the namespaces we'd like to apply assets = {} asset_namespaces = defaultdict(set) for item in items: asset_id = str(item["asset"]["_id"]) asset_name = get_asset_name_identifier(item["asset"]) asset_namespaces[asset_name].add(item.get("namespace")) if asset_name in assets: continue assets[asset_name] = item assets[asset_name]["nodes"] = id_nodes.get(asset_id, []) # Filter nodes to namespace (if only namespaces were selected) for asset_name in assets: namespaces = asset_namespaces[asset_name] # When None is present there should be no filtering if None in namespaces: continue # Else only namespaces are selected and *not* the top entry so # we should filter to only those namespaces. nodes = assets[asset_name]["nodes"] nodes = [node for node in nodes if commands.get_namespace_from_node(node) in namespaces] assets[asset_name]["nodes"] = nodes return assets def select_asset_from_items(self): """Select nodes from listed asset""" items = self.get_nodes(selection=False) nodes = [] for item in items.values(): nodes.extend(item["nodes"]) commands.select(nodes) def right_mouse_menu(self, pos): """Build RMB menu for asset outliner""" active = self.view.currentIndex() # index under mouse active = active.sibling(active.row(), 0) # get first column globalpos = self.view.viewport().mapToGlobal(pos) menu = QtWidgets.QMenu(self.view) # Direct assignment apply_action = QtWidgets.QAction(menu, text="Select nodes") apply_action.triggered.connect(self.select_asset_from_items) if not active.isValid(): apply_action.setEnabled(False) menu.addAction(apply_action) menu.exec_(globalpos) class LookOutliner(QtWidgets.QWidget): menu_apply_action = QtCore.Signal() def __init__(self, parent=None): super(LookOutliner, self).__init__(parent) # Looks from database title = QtWidgets.QLabel("Looks", self) title.setAlignment(QtCore.Qt.AlignCenter) title.setStyleSheet("font-weight: bold; font-size: 12px") title.setAlignment(QtCore.Qt.AlignCenter) model = LookModel() # Proxy for dynamic sorting proxy = QtCore.QSortFilterProxyModel() proxy.setSourceModel(model) view = View(self) view.setModel(proxy) view.setMinimumHeight(180) view.setToolTip("Use right mouse button menu for direct actions") view.customContextMenuRequested.connect(self.right_mouse_menu) view.sortByColumn(0, QtCore.Qt.AscendingOrder) # look manager layout layout = QtWidgets.QVBoxLayout(self) layout.setContentsMargins(0, 0, 0, 0) layout.setSpacing(10) layout.addWidget(title) layout.addWidget(view) self.view = view self.model = model def clear(self): self.model.clear() def add_items(self, items): self.model.add_items(items) def get_selected_items(self): """Get current selected items from view Returns: list: list of dictionaries """ items = [i.data(TreeModel.ItemRole) for i in self.view.get_indices()] return [item for item in items if item is not None] def right_mouse_menu(self, pos): """Build RMB menu for look view""" active = self.view.currentIndex() # index under mouse active = active.sibling(active.row(), 0) # get first column globalpos = self.view.viewport().mapToGlobal(pos) if not active.isValid(): return menu = QtWidgets.QMenu(self.view) # Direct assignment apply_action = QtWidgets.QAction(menu, text="Assign looks..") apply_action.triggered.connect(self.menu_apply_action) menu.addAction(apply_action) menu.exec_(globalpos) ================================================ FILE: openpype/hosts/nuke/__init__.py ================================================ from .addon import ( NUKE_ROOT_DIR, NukeAddon, ) __all__ = ( "NUKE_ROOT_DIR", "NukeAddon", ) ================================================ FILE: openpype/hosts/nuke/addon.py ================================================ import os import platform from openpype.modules import OpenPypeModule, IHostAddon NUKE_ROOT_DIR = os.path.dirname(os.path.abspath(__file__)) class NukeAddon(OpenPypeModule, IHostAddon): name = "nuke" host_name = "nuke" def initialize(self, module_settings): self.enabled = True def add_implementation_envs(self, env, _app): # Add requirements to NUKE_PATH new_nuke_paths = [ os.path.join(NUKE_ROOT_DIR, "startup") ] old_nuke_path = env.get("NUKE_PATH") or "" for path in old_nuke_path.split(os.pathsep): if not path: continue norm_path = os.path.normpath(path) if norm_path not in new_nuke_paths: new_nuke_paths.append(norm_path) env["NUKE_PATH"] = os.pathsep.join(new_nuke_paths) # Remove auto screen scale factor for Qt # - let Nuke decide it's value env.pop("QT_AUTO_SCREEN_SCALE_FACTOR", None) # Remove tkinter library paths if are set env.pop("TK_LIBRARY", None) env.pop("TCL_LIBRARY", None) # Add vendor to PYTHONPATH python_path = env["PYTHONPATH"] python_path_parts = [] if python_path: python_path_parts = python_path.split(os.pathsep) vendor_path = os.path.join(NUKE_ROOT_DIR, "vendor") python_path_parts.insert(0, vendor_path) env["PYTHONPATH"] = os.pathsep.join(python_path_parts) # Set default values if are not already set via settings defaults = { "LOGLEVEL": "DEBUG" } for key, value in defaults.items(): if not env.get(key): env[key] = value # Try to add QuickTime to PATH quick_time_path = "C:/Program Files (x86)/QuickTime/QTSystem" if platform.system() == "windows" and os.path.exists(quick_time_path): path_value = env.get("PATH") or "" path_paths = [ path for path in path_value.split(os.pathsep) if path ] path_paths.append(quick_time_path) env["PATH"] = os.pathsep.join(path_paths) def get_launch_hook_paths(self, app): if app.host_name != self.host_name: return [] return [ os.path.join(NUKE_ROOT_DIR, "hooks") ] def get_workfile_extensions(self): return [".nk"] ================================================ FILE: openpype/hosts/nuke/api/__init__.py ================================================ from .workio import ( file_extensions, has_unsaved_changes, save_file, open_file, current_file, work_root, ) from .command import ( viewer_update_and_undo_stop ) from .plugin import ( NukeCreator, NukeWriteCreator, NukeCreatorError, OpenPypeCreator, get_instance_group_node_childs, get_colorspace_from_node ) from .pipeline import ( NukeHost, ls, list_instances, remove_instance, select_instance, containerise, parse_container, update_container, ) from .lib import ( INSTANCE_DATA_KNOB, ROOT_DATA_KNOB, maintained_selection, reset_selection, select_nodes, get_view_process_node, duplicate_node, convert_knob_value_to_correct_type, get_node_data, set_node_data, update_node_data, create_write_node, link_knobs ) from .utils import ( colorspace_exists_on_node, get_colorspace_list ) from .actions import ( SelectInvalidAction, SelectInstanceNodeAction ) __all__ = ( "file_extensions", "has_unsaved_changes", "save_file", "open_file", "current_file", "work_root", "viewer_update_and_undo_stop", "NukeCreator", "NukeWriteCreator", "NukeCreatorError", "OpenPypeCreator", "NukeHost", "get_instance_group_node_childs", "get_colorspace_from_node", "ls", "list_instances", "remove_instance", "select_instance", "containerise", "parse_container", "update_container", "INSTANCE_DATA_KNOB", "ROOT_DATA_KNOB", "maintained_selection", "reset_selection", "select_nodes", "get_view_process_node", "duplicate_node", "convert_knob_value_to_correct_type", "get_node_data", "set_node_data", "update_node_data", "create_write_node", "link_knobs", "colorspace_exists_on_node", "get_colorspace_list", "SelectInvalidAction", "SelectInstanceNodeAction" ) ================================================ FILE: openpype/hosts/nuke/api/actions.py ================================================ import pyblish.api from openpype.pipeline.publish import get_errored_instances_from_context from .lib import ( reset_selection, select_nodes ) class SelectInvalidAction(pyblish.api.Action): """Select invalid nodes in Nuke when plug-in failed. To retrieve the invalid nodes this assumes a static `get_invalid()` method is available on the plugin. """ label = "Select invalid nodes" on = "failed" # This action is only available on a failed plug-in icon = "search" # Icon from Awesome Icon def process(self, context, plugin): errored_instances = get_errored_instances_from_context(context, plugin=plugin) # Get the invalid nodes for the plug-ins self.log.info("Finding invalid nodes..") invalid = set() for instance in errored_instances: invalid_nodes = plugin.get_invalid(instance) if invalid_nodes: if isinstance(invalid_nodes, (list, tuple)): invalid.update(invalid_nodes) else: self.log.warning("Plug-in returned to be invalid, " "but has no selectable nodes.") if invalid: self.log.info("Selecting invalid nodes: {}".format(invalid)) reset_selection() select_nodes(invalid) else: self.log.info("No invalid nodes found.") class SelectInstanceNodeAction(pyblish.api.Action): """Select instance node for failed plugin.""" label = "Select instance node" on = "failed" # This action is only available on a failed plug-in icon = "mdi.cursor-default-click" def process(self, context, plugin): # Get the errored instances for the plug-in errored_instances = get_errored_instances_from_context( context, plugin) # Get the invalid nodes for the plug-ins self.log.info("Finding instance nodes..") nodes = set() for instance in errored_instances: instance_node = instance.data.get("transientData", {}).get("node") if not instance_node: raise RuntimeError( "No transientData['node'] found on instance: {}".format( instance ) ) nodes.add(instance_node) if nodes: self.log.info("Selecting instance nodes: {}".format(nodes)) reset_selection() select_nodes(nodes) else: self.log.info("No instance nodes found.") ================================================ FILE: openpype/hosts/nuke/api/command.py ================================================ import logging import contextlib import nuke log = logging.getLogger(__name__) @contextlib.contextmanager def viewer_update_and_undo_stop(): """Lock viewer from updating and stop recording undo steps""" try: # stop active viewer to update any change viewer = nuke.activeViewer() if viewer: viewer.stop() else: log.warning("No available active Viewer") nuke.Undo.disable() yield finally: nuke.Undo.enable() ================================================ FILE: openpype/hosts/nuke/api/constants.py ================================================ import os ASSIST = bool(os.getenv("NUKEASSIST")) ================================================ FILE: openpype/hosts/nuke/api/gizmo_menu.py ================================================ import os import re import nuke from openpype.lib import Logger log = Logger.get_logger(__name__) class GizmoMenu(): def __init__(self, title, icon=None): self.toolbar = self._create_toolbar_menu( title, icon=icon ) self._script_actions = [] def _create_toolbar_menu(self, name, icon=None): nuke_node_menu = nuke.menu("Nodes") return nuke_node_menu.addMenu( name, icon=icon ) def _make_menu_path(self, path, icon=None): parent = self.toolbar for folder in re.split(r"/|\\", path): if not folder: continue existing_menu = parent.findItem(folder) if existing_menu: parent = existing_menu else: parent = parent.addMenu(folder, icon=icon) return parent def build_from_configuration(self, configuration): for menu in configuration: # Construct parent path else parent is toolbar parent = self.toolbar gizmo_toolbar_path = menu.get("gizmo_toolbar_path") if gizmo_toolbar_path: parent = self._make_menu_path(gizmo_toolbar_path) for item in menu["sub_gizmo_list"]: assert isinstance(item, dict), "Configuration is wrong!" if not item.get("title"): continue item_type = item.get("sourcetype") if item_type == "python": parent.addCommand( item["title"], command=str(item["command"]), icon=item.get("icon"), shortcut=item.get("shortcut") ) elif item_type == "file": parent.addCommand( item['title'], "nuke.createNode('{}')".format(item.get('file_name')), shortcut=item.get('shortcut') ) # add separator # Special behavior for separators elif item_type == "separator": parent.addSeparator() # add submenu # items should hold a collection of submenu items (dict) elif item_type == "menu": # assert "items" in item, "Menu is missing 'items' key" parent.addMenu( item['title'], icon=item.get('icon') ) def add_gizmo_path(self, gizmo_paths): for gizmo_path in gizmo_paths: if os.path.isdir(gizmo_path): for folder in os.listdir(gizmo_path): if os.path.isdir(os.path.join(gizmo_path, folder)): nuke.pluginAddPath(os.path.join(gizmo_path, folder)) nuke.pluginAddPath(gizmo_path) else: log.warning("This path doesn't exist: {}".format(gizmo_path)) ================================================ FILE: openpype/hosts/nuke/api/lib.py ================================================ import os from pprint import pformat import re import json import six import functools import warnings import platform import tempfile import contextlib from collections import OrderedDict import nuke from qtpy import QtCore, QtWidgets from openpype import AYON_SERVER_ENABLED from openpype.client import ( get_project, get_asset_by_name, get_versions, get_last_versions, get_representations, ) from openpype.host import HostDirmap from openpype.tools.utils import host_tools from openpype.pipeline.workfile.workfile_template_builder import ( TemplateProfileNotFound ) from openpype.lib import ( env_value_to_bool, Logger, get_version_from_path, StringTemplate, ) from openpype.settings import ( get_project_settings, get_current_project_settings, ) from openpype.modules import ModulesManager from openpype.pipeline.template_data import get_template_data_with_names from openpype.pipeline import ( discover_legacy_creator_plugins, Anatomy, get_current_host_name, get_current_project_name, get_current_asset_name, ) from openpype.pipeline.context_tools import ( get_custom_workfile_template_from_session ) from openpype.pipeline.colorspace import get_imageio_config from openpype.pipeline.workfile import BuildWorkfile from . import gizmo_menu from .constants import ASSIST from .workio import save_file from .utils import get_node_outputs log = Logger.get_logger(__name__) _NODE_TAB_NAME = "{}".format(os.getenv("AVALON_LABEL") or "Avalon") AVALON_LABEL = os.getenv("AVALON_LABEL") or "Avalon" AVALON_TAB = "{}".format(AVALON_LABEL) AVALON_DATA_GROUP = "{}DataGroup".format(AVALON_LABEL.capitalize()) EXCLUDED_KNOB_TYPE_ON_READ = ( 20, # Tab Knob 26, # Text Knob (But for backward compatibility, still be read # if value is not an empty string.) ) JSON_PREFIX = "JSON:::" ROOT_DATA_KNOB = "publish_context" INSTANCE_DATA_KNOB = "publish_instance" class DeprecatedWarning(DeprecationWarning): pass def deprecated(new_destination): """Mark functions as deprecated. It will result in a warning being emitted when the function is used. """ func = None if callable(new_destination): func = new_destination new_destination = None def _decorator(decorated_func): if new_destination is None: warning_message = ( " Please check content of deprecated function to figure out" " possible replacement." ) else: warning_message = " Please replace your usage with '{}'.".format( new_destination ) @functools.wraps(decorated_func) def wrapper(*args, **kwargs): warnings.simplefilter("always", DeprecatedWarning) warnings.warn( ( "Call to deprecated function '{}'" "\nFunction was moved or removed.{}" ).format(decorated_func.__name__, warning_message), category=DeprecatedWarning, stacklevel=4 ) return decorated_func(*args, **kwargs) return wrapper if func is None: return _decorator return _decorator(func) class Context: main_window = None context_action_item = None project_name = os.getenv("AVALON_PROJECT") # Workfile related code workfiles_launched = False workfiles_tool_timer = None # Seems unused _project_doc = None def get_main_window(): """Acquire Nuke's main window""" if Context.main_window is None: top_widgets = QtWidgets.QApplication.topLevelWidgets() name = "Foundry::UI::DockMainWindow" for widget in top_widgets: if ( widget.inherits("QMainWindow") and widget.metaObject().className() == name ): Context.main_window = widget break return Context.main_window def set_node_data(node, knobname, data): """Write data to node invisible knob Will create new in case it doesn't exists or update the one already created. Args: node (nuke.Node): node object knobname (str): knob name data (dict): data to be stored in knob """ # if exists then update data if knobname in node.knobs(): log.debug("Updating knobname `{}` on node `{}`".format( knobname, node.name() )) update_node_data(node, knobname, data) return log.debug("Creating knobname `{}` on node `{}`".format( knobname, node.name() )) # else create new knob_value = JSON_PREFIX + json.dumps(data) knob = nuke.String_Knob(knobname) knob.setValue(knob_value) knob.setFlag(nuke.INVISIBLE) node.addKnob(knob) def get_node_data(node, knobname): """Read data from node. Args: node (nuke.Node): node object knobname (str): knob name Returns: dict: data stored in knob """ if knobname not in node.knobs(): return rawdata = node[knobname].getValue() if ( isinstance(rawdata, six.string_types) and rawdata.startswith(JSON_PREFIX) ): try: return json.loads(rawdata[len(JSON_PREFIX):]) except json.JSONDecodeError: return def update_node_data(node, knobname, data): """Update already present data. Args: node (nuke.Node): node object knobname (str): knob name data (dict): data to update knob value """ knob = node[knobname] node_data = get_node_data(node, knobname) or {} node_data.update(data) knob_value = JSON_PREFIX + json.dumps(node_data) knob.setValue(knob_value) class Knobby(object): """[DEPRECATED] For creating knob which it's type isn't mapped in `create_knobs` Args: type (string): Nuke knob type name value: Value to be set with `Knob.setValue`, put `None` if not required flags (list, optional): Knob flags to be set with `Knob.setFlag` *args: Args other than knob name for initializing knob class """ def __init__(self, type, value, flags=None, *args): self.type = type self.value = value self.flags = flags or [] self.args = args def create(self, name, nice=None): knob_cls = getattr(nuke, self.type) knob = knob_cls(name, nice, *self.args) if self.value is not None: knob.setValue(self.value) for flag in self.flags: knob.setFlag(flag) return knob @staticmethod def nice_naming(key): """Convert camelCase name into UI Display Name""" words = re.findall('[A-Z][^A-Z]*', key[0].upper() + key[1:]) return " ".join(words) def create_knobs(data, tab=None): """Create knobs by data Depending on the type of each dict value and creates the correct Knob. Mapped types: bool: nuke.Boolean_Knob int: nuke.Int_Knob float: nuke.Double_Knob list: nuke.Enumeration_Knob six.string_types: nuke.String_Knob dict: If it's a nested dict (all values are dict), will turn into A tabs group. Or just a knobs group. Args: data (dict): collection of attributes and their value tab (string, optional): Knobs' tab name Returns: list: A list of `nuke.Knob` objects """ def nice_naming(key): """Convert camelCase name into UI Display Name""" words = re.findall('[A-Z][^A-Z]*', key[0].upper() + key[1:]) return " ".join(words) # Turn key-value pairs into knobs knobs = list() if tab: knobs.append(nuke.Tab_Knob(tab)) for key, value in data.items(): # Knob name if isinstance(key, tuple): name, nice = key else: name, nice = key, nice_naming(key) # Create knob by value type if isinstance(value, Knobby): knobby = value knob = knobby.create(name, nice) elif isinstance(value, float): knob = nuke.Double_Knob(name, nice) knob.setValue(value) elif isinstance(value, bool): knob = nuke.Boolean_Knob(name, nice) knob.setValue(value) knob.setFlag(nuke.STARTLINE) elif isinstance(value, int): knob = nuke.Int_Knob(name, nice) knob.setValue(value) elif isinstance(value, six.string_types): knob = nuke.String_Knob(name, nice) knob.setValue(value) elif isinstance(value, list): knob = nuke.Enumeration_Knob(name, nice, value) elif isinstance(value, dict): if all(isinstance(v, dict) for v in value.values()): # Create a group of tabs begain = nuke.BeginTabGroup_Knob() end = nuke.EndTabGroup_Knob() begain.setName(name) end.setName(name + "_End") knobs.append(begain) for k, v in value.items(): knobs += create_knobs(v, tab=k) knobs.append(end) else: # Create a group of knobs knobs.append(nuke.Tab_Knob( name, nice, nuke.TABBEGINCLOSEDGROUP)) knobs += create_knobs(value) knobs.append( nuke.Tab_Knob(name + "_End", nice, nuke.TABENDGROUP)) continue else: raise TypeError("Unsupported type: %r" % type(value)) knobs.append(knob) return knobs def imprint(node, data, tab=None): """Store attributes with value on node Parse user data into Node knobs. Use `collections.OrderedDict` to ensure knob order. Args: node(nuke.Node): node object from Nuke data(dict): collection of attributes and their value Returns: None Examples: ``` import nuke from openpype.hosts.nuke.api import lib node = nuke.createNode("NoOp") data = { # Regular type of attributes "myList": ["x", "y", "z"], "myBool": True, "myFloat": 0.1, "myInt": 5, # Creating non-default imprint type of knob "MyFilePath": lib.Knobby("File_Knob", "/file/path"), "divider": lib.Knobby("Text_Knob", ""), # Manual nice knob naming ("my_knob", "Nice Knob Name"): "some text", # dict type will be created as knob group "KnobGroup": { "knob1": 5, "knob2": "hello", "knob3": ["a", "b"], }, # Nested dict will be created as tab group "TabGroup": { "tab1": {"count": 5}, "tab2": {"isGood": True}, "tab3": {"direction": ["Left", "Right"]}, }, } lib.imprint(node, data, tab="Demo") ``` """ for knob in create_knobs(data, tab): node.addKnob(knob) @deprecated def add_publish_knob(node): """[DEPRECATED] Add Publish knob to node Arguments: node (nuke.Node): nuke node to be processed Returns: node (nuke.Node): processed nuke node """ if "publish" not in node.knobs(): body = OrderedDict() body[("divd", "Publishing")] = Knobby("Text_Knob", '') body["publish"] = True imprint(node, body) return node @deprecated("openpype.hosts.nuke.api.lib.set_node_data") def set_avalon_knob_data(node, data=None, prefix="avalon:"): """[DEPRECATED] Sets data into nodes's avalon knob This function is still used but soon will be deprecated. Use `set_node_data` instead. Arguments: node (nuke.Node): Nuke node to imprint with data, data (dict, optional): Data to be imprinted into AvalonTab prefix (str, optional): filtering prefix Returns: node (nuke.Node) Examples: data = { 'asset': 'sq020sh0280', 'family': 'render', 'subset': 'subsetMain' } """ data = data or dict() create = OrderedDict() tab_name = AVALON_TAB editable = ["asset", "subset", "name", "namespace"] existed_knobs = node.knobs() for key, value in data.items(): knob_name = prefix + key gui_name = key if knob_name in existed_knobs: # Set value try: node[knob_name].setValue(value) except TypeError: node[knob_name].setValue(str(value)) else: # New knob name = (knob_name, gui_name) # Hide prefix on GUI if key in editable: create[name] = value else: create[name] = Knobby("String_Knob", str(value), flags=[nuke.READ_ONLY]) if tab_name in existed_knobs: tab_name = None else: tab = OrderedDict() warn = Knobby("Text_Knob", "Warning! Do not change following data!") divd = Knobby("Text_Knob", "") head = [ (("warn", ""), warn), (("divd", ""), divd), ] tab[AVALON_DATA_GROUP] = OrderedDict(head + list(create.items())) create = tab imprint(node, create, tab=tab_name) return node @deprecated("openpype.hosts.nuke.api.lib.get_node_data") def get_avalon_knob_data(node, prefix="avalon:", create=True): """[DEPRECATED] Gets a data from nodes's avalon knob This function is still used but soon will be deprecated. Use `get_node_data` instead. Arguments: node (obj): Nuke node to search for data, prefix (str, optional): filtering prefix Returns: data (dict) """ data = {} if AVALON_TAB not in node.knobs(): return data # check if lists if not isinstance(prefix, list): prefix = [prefix] # loop prefix for p in prefix: # check if the node is avalon tracked try: # check if data available on the node test = node[AVALON_DATA_GROUP].value() log.debug("Only testing if data available: `{}`".format(test)) except NameError as e: # if it doesn't then create it log.debug("Creating avalon knob: `{}`".format(e)) if create: node = set_avalon_knob_data(node) return get_avalon_knob_data(node) return {} # get data from filtered knobs data.update({k.replace(p, ''): node[k].value() for k in node.knobs().keys() if p in k}) return data @deprecated def fix_data_for_node_create(data): """[DEPRECATED] Fixing data to be used for nuke knobs """ for k, v in data.items(): if isinstance(v, six.text_type): data[k] = str(v) if str(v).startswith("0x"): data[k] = int(v, 16) return data @deprecated def add_write_node_legacy(name, **kwarg): """[DEPRECATED] Adding nuke write node Arguments: name (str): nuke node name kwarg (attrs): data for nuke knobs Returns: node (obj): nuke write node """ use_range_limit = kwarg.get("use_range_limit", None) w = nuke.createNode( "Write", "name {}".format(name), inpanel=False ) w["file"].setValue(kwarg["file"]) for k, v in kwarg.items(): if "frame_range" in k: continue log.info([k, v]) try: w[k].setValue(v) except KeyError as e: log.debug(e) continue if use_range_limit: w["use_limit"].setValue(True) w["first"].setValue(kwarg["frame_range"][0]) w["last"].setValue(kwarg["frame_range"][1]) return w def add_write_node(name, file_path, knobs, **kwarg): """Adding nuke write node Arguments: name (str): nuke node name kwarg (attrs): data for nuke knobs Returns: node (obj): nuke write node """ use_range_limit = kwarg.get("use_range_limit", None) w = nuke.createNode( "Write", "name {}".format(name), inpanel=False ) w["file"].setValue(file_path) # finally add knob overrides set_node_knobs_from_settings(w, knobs, **kwarg) if use_range_limit: w["use_limit"].setValue(True) w["first"].setValue(kwarg["frame_range"][0]) w["last"].setValue(kwarg["frame_range"][1]) return w def read_avalon_data(node): """Return user-defined knobs from given `node` Args: node (nuke.Node): Nuke node object Returns: list: A list of nuke.Knob object """ def compat_prefixed(knob_name): if knob_name.startswith("avalon:"): return knob_name[len("avalon:"):] elif knob_name.startswith("ak:"): return knob_name[len("ak:"):] data = dict() pattern = ("(?<=addUserKnob {)" "([0-9]*) (\\S*)" # Matching knob type and knob name "(?=[ |}])") tcl_script = node.writeKnobs(nuke.WRITE_USER_KNOB_DEFS) result = re.search(pattern, tcl_script) if result: first_user_knob = result.group(2) # Collect user knobs from the end of the knob list for knob in reversed(node.allKnobs()): knob_name = knob.name() if not knob_name: # Ignore unnamed knob continue knob_type = nuke.knob(knob.fullyQualifiedName(), type=True) value = knob.value() if ( knob_type not in EXCLUDED_KNOB_TYPE_ON_READ or # For compating read-only string data that imprinted # by `nuke.Text_Knob`. (knob_type == 26 and value) ): key = compat_prefixed(knob_name) if key is not None: data[key] = value if knob_name == first_user_knob: break return data def get_node_path(path, padding=4): """Get filename for the Nuke write with padded number as '#' Arguments: path (str): The path to render to. Returns: tuple: head, padding, tail (extension) Examples: >>> get_frame_path("test.exr") ('test', 4, '.exr') >>> get_frame_path("filename.#####.tif") ('filename.', 5, '.tif') >>> get_frame_path("foobar##.tif") ('foobar', 2, '.tif') >>> get_frame_path("foobar_%08d.tif") ('foobar_', 8, '.tif') """ filename, ext = os.path.splitext(path) # Find a final number group if '%' in filename: match = re.match('.*?(%[0-9]+d)$', filename) if match: padding = int(match.group(1).replace('%', '').replace('d', '')) # remove number from end since fusion # will swap it with the frame number filename = filename.replace(match.group(1), '') elif '#' in filename: match = re.match('.*?(#+)$', filename) if match: padding = len(match.group(1)) # remove number from end since fusion # will swap it with the frame number filename = filename.replace(match.group(1), '') return filename, padding, ext def get_nuke_imageio_settings(): return get_project_settings(Context.project_name)["nuke"]["imageio"] @deprecated("openpype.hosts.nuke.api.lib.get_nuke_imageio_settings") def get_created_node_imageio_setting_legacy(nodeclass, creator, subset): '''[DEPRECATED] Get preset data for dataflow (fileType, compression, bitDepth) ''' assert any([creator, nodeclass]), nuke.message( "`{}`: Missing mandatory kwargs `host`, `cls`".format(__file__)) imageio_nodes = get_nuke_imageio_settings()["nodes"] required_nodes = imageio_nodes["requiredNodes"] # HACK: for backward compatibility this needs to be optional override_nodes = imageio_nodes.get("overrideNodes", []) imageio_node = None for node in required_nodes: log.info(node) if ( nodeclass in node["nukeNodeClass"] and creator in node["plugins"] ): imageio_node = node break log.debug("__ imageio_node: {}".format(imageio_node)) # find matching override node override_imageio_node = None for onode in override_nodes: log.info(onode) if nodeclass not in node["nukeNodeClass"]: continue if creator not in node["plugins"]: continue if ( onode["subsets"] and not any( re.search(s.lower(), subset.lower()) for s in onode["subsets"] ) ): continue override_imageio_node = onode break log.debug("__ override_imageio_node: {}".format(override_imageio_node)) # add overrides to imageio_node if override_imageio_node: # get all knob names in imageio_node knob_names = [k["name"] for k in imageio_node["knobs"]] for oknob in override_imageio_node["knobs"]: for knob in imageio_node["knobs"]: # override matching knob name if oknob["name"] == knob["name"]: log.debug( "_ overriding knob: `{}` > `{}`".format( knob, oknob )) if not oknob["value"]: # remove original knob if no value found in oknob imageio_node["knobs"].remove(knob) else: # override knob value with oknob's knob["value"] = oknob["value"] # add missing knobs into imageio_node if oknob["name"] not in knob_names: log.debug( "_ adding knob: `{}`".format(oknob)) imageio_node["knobs"].append(oknob) knob_names.append(oknob["name"]) log.info("ImageIO node: {}".format(imageio_node)) return imageio_node def get_imageio_node_setting(node_class, plugin_name, subset): ''' Get preset data for dataflow (fileType, compression, bitDepth) ''' imageio_nodes = get_nuke_imageio_settings()["nodes"] required_nodes = imageio_nodes["requiredNodes"] imageio_node = None for node in required_nodes: log.info(node) if ( node_class in node["nukeNodeClass"] and plugin_name in node["plugins"] ): imageio_node = node break log.debug("__ imageio_node: {}".format(imageio_node)) if not imageio_node: return # find overrides and update knobs with them get_imageio_node_override_setting( node_class, plugin_name, subset, imageio_node["knobs"] ) log.info("ImageIO node: {}".format(imageio_node)) return imageio_node def get_imageio_node_override_setting( node_class, plugin_name, subset, knobs_settings ): ''' Get imageio node overrides from settings ''' imageio_nodes = get_nuke_imageio_settings()["nodes"] override_nodes = imageio_nodes["overrideNodes"] # find matching override node override_imageio_node = None for onode in override_nodes: log.debug("__ onode: {}".format(onode)) log.debug("__ subset: {}".format(subset)) if node_class not in onode["nukeNodeClass"]: continue if plugin_name not in onode["plugins"]: continue if ( onode["subsets"] and not any( re.search(s.lower(), subset.lower()) for s in onode["subsets"] ) ): continue override_imageio_node = onode break log.debug("__ override_imageio_node: {}".format(override_imageio_node)) # add overrides to imageio_node if override_imageio_node: # get all knob names in imageio_node knob_names = [k["name"] for k in knobs_settings] for oknob in override_imageio_node["knobs"]: for knob in knobs_settings: # override matching knob name if oknob["name"] == knob["name"]: log.debug( "_ overriding knob: `{}` > `{}`".format( knob, oknob )) if not oknob["value"]: # remove original knob if no value found in oknob knobs_settings.remove(knob) else: # override knob value with oknob's knob["value"] = oknob["value"] # add missing knobs into imageio_node if oknob["name"] not in knob_names: log.debug( "_ adding knob: `{}`".format(oknob)) knobs_settings.append(oknob) knob_names.append(oknob["name"]) return knobs_settings def get_imageio_input_colorspace(filename): ''' Get input file colorspace based on regex in settings. ''' imageio_regex_inputs = ( get_nuke_imageio_settings()["regexInputs"]["inputs"]) preset_clrsp = None for regexInput in imageio_regex_inputs: if bool(re.search(regexInput["regex"], filename)): preset_clrsp = str(regexInput["colorspace"]) return preset_clrsp def get_view_process_node(): reset_selection() ipn_node = None for v_ in nuke.allNodes(filter="Viewer"): ipn = v_['input_process_node'].getValue() ipn_node = nuke.toNode(ipn) # skip if no input node is set if not ipn: continue if ipn == "VIEWER_INPUT" and not ipn_node: # since it is set by default we can ignore it # nobody usually use this but use it if # it exists in nodes continue if not ipn_node: # in case a Viewer node is transferred from # different workfile with old values raise NameError(( "Input process node name '{}' set in " "Viewer '{}' is doesn't exists in nodes" ).format(ipn, v_.name())) ipn_node.setSelected(True) if ipn_node: return duplicate_node(ipn_node) def on_script_load(): ''' Callback for ffmpeg support ''' if nuke.env["LINUX"]: nuke.tcl('load ffmpegReader') nuke.tcl('load ffmpegWriter') else: nuke.tcl('load movReader') nuke.tcl('load movWriter') def check_inventory_versions(): """ Actual version idetifier of Loaded containers Any time this function is run it will check all nodes and filter only Loader nodes for its version. It will get all versions from database and check if the node is having actual version. If not then it will color it to red. """ from .pipeline import parse_container # get all Loader nodes by avalon attribute metadata node_with_repre_id = [] repre_ids = set() # Find all containers and collect it's node and representation ids for node in nuke.allNodes(): container = parse_container(node) if container: node = nuke.toNode(container["objectName"]) avalon_knob_data = read_avalon_data(node) repre_id = avalon_knob_data["representation"] repre_ids.add(repre_id) node_with_repre_id.append((node, repre_id)) # Skip if nothing was found if not repre_ids: return project_name = get_current_project_name() # Find representations based on found containers repre_docs = get_representations( project_name, representation_ids=repre_ids, fields=["_id", "parent"] ) # Store representations by id and collect version ids repre_docs_by_id = {} version_ids = set() for repre_doc in repre_docs: # Use stringed representation id to match value in containers repre_id = str(repre_doc["_id"]) repre_docs_by_id[repre_id] = repre_doc version_ids.add(repre_doc["parent"]) version_docs = get_versions( project_name, version_ids, fields=["_id", "name", "parent"] ) # Store versions by id and collect subset ids version_docs_by_id = {} subset_ids = set() for version_doc in version_docs: version_docs_by_id[version_doc["_id"]] = version_doc subset_ids.add(version_doc["parent"]) # Query last versions based on subset ids last_versions_by_subset_id = get_last_versions( project_name, subset_ids=subset_ids, fields=["_id", "parent"] ) # Loop through collected container nodes and their representation ids for item in node_with_repre_id: # Some python versions of nuke can't unfold tuple in for loop node, repre_id = item repre_doc = repre_docs_by_id.get(repre_id) # Failsafe for not finding the representation. if not repre_doc: log.warning(( "Could not find the representation on node \"{}\"" ).format(node.name())) continue version_id = repre_doc["parent"] version_doc = version_docs_by_id.get(version_id) if not version_doc: log.warning(( "Could not find the version on node \"{}\"" ).format(node.name())) continue # Get last version based on subset id subset_id = version_doc["parent"] last_version = last_versions_by_subset_id[subset_id] # Check if last version is same as current version if last_version["_id"] == version_doc["_id"]: color_value = "0x4ecd25ff" else: color_value = "0xd84f20ff" node["tile_color"].setValue(int(color_value, 16)) def writes_version_sync(): ''' Callback synchronizing version of publishable write nodes ''' try: rootVersion = get_version_from_path(nuke.root().name()) padding = len(rootVersion) new_version = "v" + str("{" + ":0>{}".format(padding) + "}").format( int(rootVersion) ) log.debug("new_version: {}".format(new_version)) except Exception: return for each in nuke.allNodes(filter="Write"): # check if the node is avalon tracked if _NODE_TAB_NAME not in each.knobs(): continue avalon_knob_data = read_avalon_data(each) try: if avalon_knob_data["families"] not in ["render"]: log.debug(avalon_knob_data["families"]) continue node_file = each["file"].value() node_version = "v" + get_version_from_path(node_file) log.debug("node_version: {}".format(node_version)) node_new_file = node_file.replace(node_version, new_version) each["file"].setValue(node_new_file) if not os.path.isdir(os.path.dirname(node_new_file)): log.warning("Path does not exist! I am creating it.") os.makedirs(os.path.dirname(node_new_file)) except Exception as e: log.warning( "Write node: `{}` has no version in path: {}".format( each.name(), e)) def version_up_script(): ''' Raising working script's version ''' import nukescripts nukescripts.script_and_write_nodes_version_up() def check_subsetname_exists(nodes, subset_name): """ Checking if node is not already created to secure there is no duplicity Arguments: nodes (list): list of nuke.Node objects subset_name (str): name we try to find Returns: bool: True of False """ return next((True for n in nodes if subset_name in read_avalon_data(n).get("subset", "")), False) def format_anatomy(data): ''' Helping function for formatting of anatomy paths Arguments: data (dict): dictionary with attributes used for formatting Return: path (str) ''' project_name = get_current_project_name() anatomy = Anatomy(project_name) log.debug("__ anatomy.templates: {}".format(anatomy.templates)) padding = None if "frame_padding" in anatomy.templates.keys(): padding = int(anatomy.templates["frame_padding"]) elif "render" in anatomy.templates.keys(): padding = int( anatomy.templates["render"].get( "frame_padding" ) ) version = data.get("version", None) if not version: file = script_name() data["version"] = get_version_from_path(file) if AYON_SERVER_ENABLED: asset_name = data["folderPath"] else: asset_name = data["asset"] task_name = data["task"] host_name = get_current_host_name() context_data = get_template_data_with_names( project_name, asset_name, task_name, host_name ) data.update(context_data) data.update({ "subset": data["subset"], "family": data["family"], "frame": "#" * padding, }) return anatomy.format(data) def script_name(): ''' Returns nuke script path ''' return nuke.root().knob("name").value() def add_button_write_to_read(node): name = "createReadNode" label = "Read From Rendered" value = "import write_to_read;\ write_to_read.write_to_read(nuke.thisNode(), allow_relative=False)" knob = nuke.PyScript_Knob(name, label, value) knob.clearFlag(nuke.STARTLINE) node.addKnob(knob) def add_button_clear_rendered(node, path): name = "clearRendered" label = "Clear Rendered" value = "import clear_rendered;\ clear_rendered.clear_rendered(\"{}\")".format(path) knob = nuke.PyScript_Knob(name, label, value) node.addKnob(knob) def create_prenodes( prev_node, nodes_setting, plugin_name=None, subset=None, **kwargs ): last_node = None for_dependency = {} for name, node in nodes_setting.items(): # get attributes nodeclass = node["nodeclass"] knobs = node["knobs"] # create node now_node = nuke.createNode( nodeclass, "name {}".format(name), inpanel=False ) # add for dependency linking for_dependency[name] = { "node": now_node, "dependent": node["dependent"] } if all([plugin_name, subset]): # find imageio overrides get_imageio_node_override_setting( now_node.Class(), plugin_name, subset, knobs ) # add data to knob set_node_knobs_from_settings(now_node, knobs, **kwargs) # switch actual node to previous last_node = now_node for _node_name, node_prop in for_dependency.items(): if not node_prop["dependent"]: node_prop["node"].setInput( 0, prev_node) elif node_prop["dependent"] in for_dependency: _prev_node = for_dependency[node_prop["dependent"]]["node"] node_prop["node"].setInput( 0, _prev_node) else: log.warning("Dependency has wrong name of node: {}".format( node_prop )) return last_node def create_write_node( name, data, input=None, prenodes=None, linked_knobs=None, **kwargs ): ''' Creating write node which is group node Arguments: name (str): name of node data (dict): creator write instance data input (node)[optional]: selected node to connect to prenodes (dict)[optional]: nodes to be created before write with dependency review (bool)[optional]: adding review knob farm (bool)[optional]: rendering workflow target kwargs (dict)[optional]: additional key arguments for formatting Example: prenodes = { "nodeName": { "nodeclass": "Reformat", "dependent": [ following_node_01, ... ], "knobs": [ { "type": "text", "name": "knobname", "value": "knob value" }, ... ] }, ... } Return: node (obj): group node with avalon data as Knobs ''' prenodes = prenodes or {} # filtering variables plugin_name = data["creator"] subset = data["subset"] # get knob settings for write node imageio_writes = get_imageio_node_setting( node_class="Write", plugin_name=plugin_name, subset=subset ) for knob in imageio_writes["knobs"]: if knob["name"] == "file_type": ext = knob["value"] data.update({ "imageio_writes": imageio_writes, "ext": ext }) anatomy_filled = format_anatomy(data) # build file path to workfiles fdir = str(anatomy_filled["work"]["folder"]).replace("\\", "/") data["work"] = fdir fpath = StringTemplate(data["fpath_template"]).format_strict(data) # create directory if not os.path.isdir(os.path.dirname(fpath)): log.warning("Path does not exist! I am creating it.") os.makedirs(os.path.dirname(fpath)) GN = nuke.createNode("Group", "name {}".format(name)) prev_node = None with GN: if input: input_name = str(input.name()).replace(" ", "") # if connected input node was defined prev_node = nuke.createNode( "Input", "name {}".format(input_name), inpanel=False ) else: # generic input node connected to nothing prev_node = nuke.createNode( "Input", "name {}".format("rgba"), inpanel=False ) # creating pre-write nodes `prenodes` last_prenode = create_prenodes( prev_node, prenodes, plugin_name, subset, **kwargs ) if last_prenode: prev_node = last_prenode # creating write node write_node = now_node = add_write_node( "inside_{}".format(name), fpath, imageio_writes["knobs"], **data ) # connect to previous node now_node.setInput(0, prev_node) # switch actual node to previous prev_node = now_node now_node = nuke.createNode("Output", "name Output1", inpanel=False) # connect to previous node now_node.setInput(0, prev_node) # add divider GN.addKnob(nuke.Text_Knob('', 'Rendering')) # Add linked knobs. linked_knob_names = [] # add input linked knobs and create group only if any input if linked_knobs: linked_knob_names.append("_grp-start_") linked_knob_names.extend(linked_knobs) linked_knob_names.append("_grp-end_") linked_knob_names.append("Render") for _k_name in linked_knob_names: if "_grp-start_" in _k_name: knob = nuke.Tab_Knob( "rnd_attr", "Rendering attributes", nuke.TABBEGINCLOSEDGROUP) GN.addKnob(knob) elif "_grp-end_" in _k_name: knob = nuke.Tab_Knob( "rnd_attr_end", "Rendering attributes", nuke.TABENDGROUP) GN.addKnob(knob) else: if "___" in _k_name: # add divider GN.addKnob(nuke.Text_Knob("")) else: # add linked knob by _k_name link = nuke.Link_Knob("") link.makeLink(write_node.name(), _k_name) link.setName(_k_name) # make render if "Render" in _k_name: link.setLabel("Render Local") link.setFlag(0x1000) GN.addKnob(link) # adding write to read button add_button_write_to_read(GN) # adding write to read button add_button_clear_rendered(GN, os.path.dirname(fpath)) # set tile color tile_color = next( iter( k["value"] for k in imageio_writes["knobs"] if "tile_color" in k["name"] ), [255, 0, 0, 255] ) GN["tile_color"].setValue( color_gui_to_int(tile_color)) return GN @deprecated("openpype.hosts.nuke.api.lib.create_write_node") def create_write_node_legacy( name, data, input=None, prenodes=None, review=True, linked_knobs=None, farm=True ): ''' Creating write node which is group node Arguments: name (str): name of node data (dict): data to be imprinted input (node): selected node to connect to prenodes (list, optional): list of lists, definitions for nodes to be created before write review (bool): adding review knob Example: prenodes = [ { "nodeName": { "class": "" # string "knobs": [ ("knobName": value), ... ], "dependent": [ following_node_01, ... ] } }, ... ] Return: node (obj): group node with avalon data as Knobs ''' knob_overrides = data.get("knobs", []) nodeclass = data["nodeclass"] creator = data["creator"] subset = data["subset"] imageio_writes = get_created_node_imageio_setting_legacy( nodeclass, creator, subset ) for knob in imageio_writes["knobs"]: if knob["name"] == "file_type": representation = knob["value"] host_name = get_current_host_name() try: data.update({ "app": host_name, "imageio_writes": imageio_writes, "representation": representation, }) anatomy_filled = format_anatomy(data) except Exception as e: msg = "problem with resolving anatomy template: {}".format(e) log.error(msg) nuke.message(msg) # build file path to workfiles fdir = str(anatomy_filled["work"]["folder"]).replace("\\", "/") fpath = data["fpath_template"].format( work=fdir, version=data["version"], subset=data["subset"], frame=data["frame"], ext=representation ) # create directory if not os.path.isdir(os.path.dirname(fpath)): log.warning("Path does not exist! I am creating it.") os.makedirs(os.path.dirname(fpath)) _data = OrderedDict({ "file": fpath }) # adding dataflow template log.debug("imageio_writes: `{}`".format(imageio_writes)) for knob in imageio_writes["knobs"]: _data[knob["name"]] = knob["value"] _data = fix_data_for_node_create(_data) log.debug("_data: `{}`".format(_data)) if "frame_range" in data.keys(): _data["frame_range"] = data.get("frame_range", None) log.debug("_data[frame_range]: `{}`".format(_data["frame_range"])) GN = nuke.createNode("Group", "name {}".format(name)) prev_node = None with GN: if input: input_name = str(input.name()).replace(" ", "") # if connected input node was defined prev_node = nuke.createNode( "Input", "name {}".format(input_name)) else: # generic input node connected to nothing prev_node = nuke.createNode( "Input", "name {}".format("rgba"), inpanel=False ) # creating pre-write nodes `prenodes` if prenodes: for node in prenodes: # get attributes pre_node_name = node["name"] klass = node["class"] knobs = node["knobs"] dependent = node["dependent"] # create node now_node = nuke.createNode( klass, "name {}".format(pre_node_name), inpanel=False ) # add data to knob for _knob in knobs: knob, value = _knob try: now_node[knob].value() except NameError: log.warning( "knob `{}` does not exist on node `{}`".format( knob, now_node["name"].value() )) continue if not knob and not value: continue log.info((knob, value)) if isinstance(value, str): if "[" in value: now_node[knob].setExpression(value) else: now_node[knob].setValue(value) # connect to previous node if dependent: if isinstance(dependent, (tuple or list)): for i, node_name in enumerate(dependent): input_node = nuke.createNode( "Input", "name {}".format(node_name), inpanel=False ) now_node.setInput(1, input_node) elif isinstance(dependent, str): input_node = nuke.createNode( "Input", "name {}".format(node_name), inpanel=False ) now_node.setInput(0, input_node) else: now_node.setInput(0, prev_node) # switch actual node to previous prev_node = now_node # creating write node write_node = now_node = add_write_node_legacy( "inside_{}".format(name), **_data ) # connect to previous node now_node.setInput(0, prev_node) # switch actual node to previous prev_node = now_node now_node = nuke.createNode("Output", "name Output1", inpanel=False) # connect to previous node now_node.setInput(0, prev_node) # imprinting group node set_avalon_knob_data(GN, data["avalon"]) add_publish_knob(GN) add_rendering_knobs(GN, farm) if review: add_review_knob(GN) # add divider GN.addKnob(nuke.Text_Knob('', 'Rendering')) # Add linked knobs. linked_knob_names = [] # add input linked knobs and create group only if any input if linked_knobs: linked_knob_names.append("_grp-start_") linked_knob_names.extend(linked_knobs) linked_knob_names.append("_grp-end_") linked_knob_names.append("Render") for _k_name in linked_knob_names: if "_grp-start_" in _k_name: knob = nuke.Tab_Knob( "rnd_attr", "Rendering attributes", nuke.TABBEGINCLOSEDGROUP) GN.addKnob(knob) elif "_grp-end_" in _k_name: knob = nuke.Tab_Knob( "rnd_attr_end", "Rendering attributes", nuke.TABENDGROUP) GN.addKnob(knob) else: if "___" in _k_name: # add divider GN.addKnob(nuke.Text_Knob("")) else: # add linked knob by _k_name link = nuke.Link_Knob("") link.makeLink(write_node.name(), _k_name) link.setName(_k_name) # make render if "Render" in _k_name: link.setLabel("Render Local") link.setFlag(0x1000) GN.addKnob(link) # adding write to read button add_button_write_to_read(GN) # adding write to read button add_button_clear_rendered(GN, os.path.dirname(fpath)) # Deadline tab. add_deadline_tab(GN) # open the our Tab as default GN[_NODE_TAB_NAME].setFlag(0) # set tile color tile_color = _data.get("tile_color", "0xff0000ff") GN["tile_color"].setValue(tile_color) # override knob values from settings for knob in knob_overrides: knob_type = knob["type"] knob_name = knob["name"] knob_value = knob["value"] if knob_name not in GN.knobs(): continue if not knob_value: continue # set correctly knob types if knob_type == "string": knob_value = str(knob_value) if knob_type == "number": knob_value = int(knob_value) if knob_type == "decimal_number": knob_value = float(knob_value) if knob_type == "bool": knob_value = bool(knob_value) if knob_type in ["2d_vector", "3d_vector", "color", "box"]: knob_value = list(knob_value) GN[knob_name].setValue(knob_value) return GN def set_node_knobs_from_settings(node, knob_settings, **kwargs): """ Overriding knob values from settings Using `schema_nuke_knob_inputs` for knob type definitions. Args: node (nuke.Node): nuke node knob_settings (list): list of dict. Keys are `type`, `name`, `value` kwargs (dict)[optional]: keys for formattable knob settings """ for knob in knob_settings: log.debug("__ knob: {}".format(pformat(knob))) knob_type = knob["type"] knob_name = knob["name"] if knob_name not in node.knobs(): continue if knob_type == "expression": knob_expression = knob["expression"] node[knob_name].setExpression( knob_expression ) continue # first deal with formattable knob settings if knob_type == "formatable": template = knob["template"] to_type = knob["to_type"] try: _knob_value = template.format( **kwargs ) except KeyError as msg: raise KeyError( "Not able to format expression: {}".format(msg)) # convert value to correct type if to_type == "2d_vector": knob_value = _knob_value.split(";").split(",") else: knob_value = _knob_value knob_type = to_type else: knob_value = knob["value"] if not knob_value: continue knob_value = convert_knob_value_to_correct_type( knob_type, knob_value) node[knob_name].setValue(knob_value) def convert_knob_value_to_correct_type(knob_type, knob_value): # first convert string types to string # just to ditch unicode if isinstance(knob_value, six.text_type): knob_value = str(knob_value) # set correctly knob types if knob_type == "bool": knob_value = bool(knob_value) elif knob_type == "decimal_number": knob_value = float(knob_value) elif knob_type == "number": knob_value = int(knob_value) elif knob_type == "text": knob_value = knob_value elif knob_type == "color_gui": knob_value = color_gui_to_int(knob_value) elif knob_type in ["2d_vector", "3d_vector", "color", "box"]: knob_value = [float(val_) for val_ in knob_value] return knob_value def color_gui_to_int(color_gui): hex_value = ( "0x{0:0>2x}{1:0>2x}{2:0>2x}{3:0>2x}").format(*color_gui) return int(hex_value, 16) @deprecated def add_rendering_knobs(node, farm=True): ''' Adds additional rendering knobs to given node Arguments: node (obj): nuke node object to be fixed Return: node (obj): with added knobs ''' knob_options = ["Use existing frames", "Local"] if farm: knob_options.append("On farm") if "render" not in node.knobs(): knob = nuke.Enumeration_Knob("render", "", knob_options) knob.clearFlag(nuke.STARTLINE) node.addKnob(knob) return node @deprecated def add_review_knob(node): ''' Adds additional review knob to given node Arguments: node (obj): nuke node object to be fixed Return: node (obj): with added knob ''' if "review" not in node.knobs(): knob = nuke.Boolean_Knob("review", "Review") knob.setValue(True) node.addKnob(knob) return node @deprecated def add_deadline_tab(node): # TODO: remove this as it is only linked to legacy create node.addKnob(nuke.Tab_Knob("Deadline")) knob = nuke.Int_Knob("deadlinePriority", "Priority") knob.setValue(50) node.addKnob(knob) knob = nuke.Int_Knob("deadlineChunkSize", "Chunk Size") knob.setValue(0) node.addKnob(knob) knob = nuke.Int_Knob("deadlineConcurrentTasks", "Concurrent tasks") # zero as default will get value from Settings during collection # instead of being an explicit user override, see precollect_write.py knob.setValue(0) node.addKnob(knob) knob = nuke.Text_Knob("divd", '') knob.setValue('') node.addKnob(knob) knob = nuke.Boolean_Knob("suspend_publish", "Suspend publish") knob.setValue(False) node.addKnob(knob) @deprecated def get_deadline_knob_names(): # TODO: remove this as it is only linked to legacy # validate_write_deadline_tab return [ "Deadline", "deadlineChunkSize", "deadlinePriority", "deadlineConcurrentTasks" ] def create_backdrop(label="", color=None, layer=0, nodes=None): """ Create Backdrop node Arguments: color (str): nuke compatible string with color code layer (int): layer of node usually used (self.pos_layer - 1) label (str): the message nodes (list): list of nodes to be wrapped into backdrop """ assert isinstance(nodes, list), "`nodes` should be a list of nodes" # Calculate bounds for the backdrop node. bdX = min([node.xpos() for node in nodes]) bdY = min([node.ypos() for node in nodes]) bdW = max([node.xpos() + node.screenWidth() for node in nodes]) - bdX bdH = max([node.ypos() + node.screenHeight() for node in nodes]) - bdY # Expand the bounds to leave a little border. Elements are offsets # for left, top, right and bottom edges respectively left, top, right, bottom = (-20, -65, 20, 60) bdX += left bdY += top bdW += (right - left) bdH += (bottom - top) bdn = nuke.createNode("BackdropNode") bdn["z_order"].setValue(layer) if color: bdn["tile_color"].setValue(int(color, 16)) bdn["xpos"].setValue(bdX) bdn["ypos"].setValue(bdY) bdn["bdwidth"].setValue(bdW) bdn["bdheight"].setValue(bdH) if label: bdn["label"].setValue(label) bdn["note_font_size"].setValue(20) return bdn class WorkfileSettings(object): """ All settings for workfile will be set This object is setting all possible root settings to the workfile. Including Colorspace, Frame ranges, Resolution format. It can set it to Root node or to any given node. Arguments: root (node): nuke's root node nodes (list): list of nuke's nodes nodes_filter (list): filtering classes for nodes """ def __init__(self, root_node=None, nodes=None, **kwargs): project_doc = kwargs.get("project") if project_doc is None: project_name = get_current_project_name() project_doc = get_project(project_name) else: project_name = project_doc["name"] Context._project_doc = project_doc self._project_name = project_name self._asset = ( kwargs.get("asset_name") or get_current_asset_name() ) self._asset_entity = get_asset_by_name(project_name, self._asset) self._root_node = root_node or nuke.root() self._nodes = self.get_nodes(nodes=nodes) self.data = kwargs def get_nodes(self, nodes=None, nodes_filter=None): if not isinstance(nodes, list) and not isinstance(nodes_filter, list): return [n for n in nuke.allNodes()] elif not isinstance(nodes, list) and isinstance(nodes_filter, list): nodes = list() for filter in nodes_filter: [nodes.append(n) for n in nuke.allNodes(filter=filter)] return nodes elif isinstance(nodes, list) and not isinstance(nodes_filter, list): return [n for n in self._nodes] elif isinstance(nodes, list) and isinstance(nodes_filter, list): for filter in nodes_filter: return [n for n in self._nodes if filter in n.Class()] def set_viewers_colorspace(self, viewer_dict): ''' Adds correct colorspace to viewer Arguments: viewer_dict (dict): adjustments from presets ''' if not isinstance(viewer_dict, dict): msg = "set_viewers_colorspace(): argument should be dictionary" log.error(msg) nuke.message(msg) return filter_knobs = [ "viewerProcess", "wipe_position" ] erased_viewers = [] for v in nuke.allNodes(filter="Viewer"): # set viewProcess to preset from settings v["viewerProcess"].setValue( str(viewer_dict["viewerProcess"]) ) if str(viewer_dict["viewerProcess"]) \ not in v["viewerProcess"].value(): copy_inputs = v.dependencies() copy_knobs = {k: v[k].value() for k in v.knobs() if k not in filter_knobs} # delete viewer with wrong settings erased_viewers.append(v["name"].value()) nuke.delete(v) # create new viewer nv = nuke.createNode("Viewer") # connect to original inputs for i, n in enumerate(copy_inputs): nv.setInput(i, n) # set copied knobs for k, v in copy_knobs.items(): print(k, v) nv[k].setValue(v) # set viewerProcess nv["viewerProcess"].setValue(str(viewer_dict["viewerProcess"])) if erased_viewers: log.warning( "Attention! Viewer nodes {} were erased." "It had wrong color profile".format(erased_viewers)) def set_root_colorspace(self, imageio_host): ''' Adds correct colorspace to root Arguments: imageio_host (dict): host colorspace configurations ''' config_data = get_imageio_config( project_name=get_current_project_name(), host_name="nuke" ) workfile_settings = imageio_host["workfile"] viewer_process_settings = imageio_host["viewer"]["viewerProcess"] if not config_data: # TODO: backward compatibility for old projects - remove later # perhaps old project overrides is having it set to older version # with use of `customOCIOConfigPath` resolved_path = None if workfile_settings.get("customOCIOConfigPath"): unresolved_path = workfile_settings["customOCIOConfigPath"] ocio_paths = unresolved_path[platform.system().lower()] for ocio_p in ocio_paths: resolved_path = str(ocio_p).format(**os.environ) if not os.path.exists(resolved_path): continue if resolved_path: # set values to root self._root_node["colorManagement"].setValue("OCIO") self._root_node["OCIO_config"].setValue("custom") self._root_node["customOCIOConfigPath"].setValue( resolved_path) else: # no ocio config found and no custom path used if self._root_node["colorManagement"].value() \ not in str(workfile_settings["colorManagement"]): self._root_node["colorManagement"].setValue( str(workfile_settings["colorManagement"])) # second set ocio version if self._root_node["OCIO_config"].value() \ not in str(workfile_settings["OCIO_config"]): self._root_node["OCIO_config"].setValue( str(workfile_settings["OCIO_config"])) else: # OCIO config path is defined from prelaunch hook self._root_node["colorManagement"].setValue("OCIO") # print previous settings in case some were found in workfile residual_path = self._root_node["customOCIOConfigPath"].value() if residual_path: log.info("Residual OCIO config path found: `{}`".format( residual_path )) # we dont need the key anymore workfile_settings.pop("customOCIOConfigPath", None) workfile_settings.pop("colorManagement", None) workfile_settings.pop("OCIO_config", None) # get monitor lut from settings respecting Nuke version differences monitor_lut = workfile_settings.pop("monitorLut", None) monitor_lut_data = self._get_monitor_settings( viewer_process_settings, monitor_lut) # set monitor related knobs luts (MonitorOut, Thumbnails) for knob, value_ in monitor_lut_data.items(): workfile_settings[knob] = value_ # then set the rest for knob, value_ in workfile_settings.items(): # skip unfilled ocio config path # it will be dict in value if isinstance(value_, dict): continue # skip empty values if not value_: continue if self._root_node[knob].value() not in value_: self._root_node[knob].setValue(str(value_)) log.debug("nuke.root()['{}'] changed to: {}".format( knob, value_)) # set ocio config path if config_data: config_path = config_data["path"].replace("\\", "/") log.info("OCIO config path found: `{}`".format( config_path)) # check if there's a mismatch between environment and settings correct_settings = self._is_settings_matching_environment( config_data) # if there's no mismatch between environment and settings if correct_settings: self._set_ocio_config_path_to_workfile(config_data) def _get_monitor_settings(self, viewer_lut, monitor_lut): """ Get monitor settings from viewer and monitor lut Args: viewer_lut (str): viewer lut string monitor_lut (str): monitor lut string Returns: dict: monitor settings """ output_data = {} m_display, m_viewer = get_viewer_config_from_string(monitor_lut) v_display, v_viewer = get_viewer_config_from_string(viewer_lut) # set monitor lut differently for nuke version 14 if nuke.NUKE_VERSION_MAJOR >= 14: output_data["monitorOutLUT"] = create_viewer_profile_string( m_viewer, m_display, path_like=False) # monitorLut=thumbnails - viewerProcess makes more sense output_data["monitorLut"] = create_viewer_profile_string( v_viewer, v_display, path_like=False) if nuke.NUKE_VERSION_MAJOR == 13: output_data["monitorOutLUT"] = create_viewer_profile_string( m_viewer, m_display, path_like=False) # monitorLut=thumbnails - viewerProcess makes more sense output_data["monitorLut"] = create_viewer_profile_string( v_viewer, v_display, path_like=True) if nuke.NUKE_VERSION_MAJOR <= 12: output_data["monitorLut"] = create_viewer_profile_string( m_viewer, m_display, path_like=True) return output_data def _is_settings_matching_environment(self, config_data): """ Check if OCIO config path is different from environment Args: config_data (dict): OCIO config data from settings Returns: bool: True if settings are matching environment, False otherwise """ current_ocio_path = os.environ["OCIO"] settings_ocio_path = config_data["path"] # normalize all paths to forward slashes current_ocio_path = current_ocio_path.replace("\\", "/") settings_ocio_path = settings_ocio_path.replace("\\", "/") if current_ocio_path != settings_ocio_path: message = """ It seems like there's a mismatch between the OCIO config path set in your Nuke settings and the actual path set in your OCIO environment. To resolve this, please follow these steps: 1. Close Nuke if it's currently open. 2. Reopen Nuke. Please note the paths for your reference: - The OCIO environment path currently set: `{env_path}` - The path in your current Nuke settings: `{settings_path}` Reopening Nuke should synchronize these paths and resolve any discrepancies. """ nuke.message( message.format( env_path=current_ocio_path, settings_path=settings_ocio_path ) ) return False return True def _set_ocio_config_path_to_workfile(self, config_data): """ Set OCIO config path to workfile Path set into nuke workfile. It is trying to replace path with environment variable if possible. If not, it will set it as it is. It also saves the script to apply the change, but only if it's not empty Untitled script. Args: config_data (dict): OCIO config data from settings """ # replace path with env var if possible ocio_path = self._replace_ocio_path_with_env_var(config_data) log.info("Setting OCIO config path to: `{}`".format( ocio_path)) self._root_node["customOCIOConfigPath"].setValue( ocio_path ) self._root_node["OCIO_config"].setValue("custom") # only save script if it's not empty if self._root_node["name"].value() != "": log.info("Saving script to apply OCIO config path change.") nuke.scriptSave() def _get_included_vars(self, config_template): """ Get all environment variables included in template Args: config_template (str): OCIO config template from settings Returns: list: list of environment variables included in template """ # resolve all environments for whitelist variables included_vars = [ "BUILTIN_OCIO_ROOT", ] # include all project root related env vars for env_var in os.environ: if env_var.startswith("OPENPYPE_PROJECT_ROOT_"): included_vars.append(env_var) # use regex to find env var in template with format {ENV_VAR} # this way we make sure only template used env vars are included env_var_regex = r"\{([A-Z0-9_]+)\}" env_var = re.findall(env_var_regex, config_template) if env_var: included_vars.append(env_var[0]) return included_vars def _replace_ocio_path_with_env_var(self, config_data): """ Replace OCIO config path with environment variable Environment variable is added as TCL expression to path. TCL expression is also replacing backward slashes found in path for windows formatted values. Args: config_data (str): OCIO config dict from settings Returns: str: OCIO config path with environment variable TCL expression """ config_path = config_data["path"].replace("\\", "/") config_template = config_data["template"] included_vars = self._get_included_vars(config_template) # make sure we return original path if no env var is included new_path = config_path for env_var in included_vars: env_path = os.getenv(env_var) if not env_path: continue # it has to be directory current process can see if not os.path.isdir(env_path): continue # make sure paths are in same format env_path = env_path.replace("\\", "/") path = config_path.replace("\\", "/") # check if env_path is in path and replace to first found positive if env_path in path: # with regsub we make sure path format of slashes is correct resub_expr = ( "[regsub -all {{\\\\}} [getenv {}] \"/\"]").format(env_var) new_path = path.replace( env_path, resub_expr ) break return new_path def set_writes_colorspace(self): ''' Adds correct colorspace to write node dict ''' for node in nuke.allNodes(filter="Group", group=self._root_node): log.info("Setting colorspace to `{}`".format(node.name())) # get data from avalon knob avalon_knob_data = read_avalon_data(node) node_data = get_node_data(node, INSTANCE_DATA_KNOB) if ( # backward compatibility # TODO: remove this once old avalon data api will be removed avalon_knob_data and avalon_knob_data.get("id") != "pyblish.avalon.instance" ): continue elif ( node_data and node_data.get("id") != "pyblish.avalon.instance" ): continue if ( # backward compatibility # TODO: remove this once old avalon data api will be removed avalon_knob_data and "creator" not in avalon_knob_data ): continue elif ( node_data and "creator_identifier" not in node_data ): continue nuke_imageio_writes = None if avalon_knob_data: # establish families families = [avalon_knob_data["family"]] if avalon_knob_data.get("families"): families.append(avalon_knob_data.get("families")) nuke_imageio_writes = get_imageio_node_setting( node_class=avalon_knob_data["families"], plugin_name=avalon_knob_data["creator"], subset=avalon_knob_data["subset"] ) elif node_data: nuke_imageio_writes = get_write_node_template_attr(node) log.debug("nuke_imageio_writes: `{}`".format(nuke_imageio_writes)) if not nuke_imageio_writes: return write_node = None # get into the group node node.begin() for x in nuke.allNodes(): if x.Class() == "Write": write_node = x node.end() if not write_node: return try: # write all knobs to node for knob in nuke_imageio_writes["knobs"]: value = knob["value"] if isinstance(value, six.text_type): value = str(value) if str(value).startswith("0x"): value = int(value, 16) log.debug("knob: {}| value: {}".format( knob["name"], value )) write_node[knob["name"]].setValue(value) except TypeError: log.warning( "Legacy workflow didn't work, switching to current") set_node_knobs_from_settings( write_node, nuke_imageio_writes["knobs"]) def set_reads_colorspace(self, read_clrs_inputs): """ Setting colorspace to Read nodes Looping through all read nodes and tries to set colorspace based on regex rules in presets """ changes = {} for n in nuke.allNodes(): file = nuke.filename(n) if n.Class() != "Read": continue # check if any colorspace presets for read is matching preset_clrsp = None for input in read_clrs_inputs: if not bool(re.search(input["regex"], file)): continue preset_clrsp = input["colorspace"] if preset_clrsp is not None: current = n["colorspace"].value() future = str(preset_clrsp) if current != future: changes[n.name()] = { "from": current, "to": future } log.debug(changes) if changes: msg = "Read nodes are not set to correct colorspace:\n\n" for nname, knobs in changes.items(): msg += ( " - node: '{0}' is now '{1}' but should be '{2}'\n" ).format(nname, knobs["from"], knobs["to"]) msg += "\nWould you like to change it?" if nuke.ask(msg): for nname, knobs in changes.items(): n = nuke.toNode(nname) n["colorspace"].setValue(knobs["to"]) log.info( "Setting `{0}` to `{1}`".format( nname, knobs["to"])) def set_colorspace(self): ''' Setting colorspace following presets ''' # get imageio nuke_colorspace = get_nuke_imageio_settings() log.info("Setting colorspace to workfile...") try: self.set_root_colorspace(nuke_colorspace) except AttributeError as _error: msg = "Set Colorspace to workfile error: {}".format(_error) nuke.message(msg) log.info("Setting colorspace to viewers...") try: self.set_viewers_colorspace(nuke_colorspace["viewer"]) except AttributeError as _error: msg = "Set Colorspace to viewer error: {}".format(_error) nuke.message(msg) log.info("Setting colorspace to write nodes...") try: self.set_writes_colorspace() except AttributeError as _error: nuke.message(_error) log.error(_error) log.info("Setting colorspace to read nodes...") read_clrs_inputs = nuke_colorspace["regexInputs"].get("inputs", []) if read_clrs_inputs: self.set_reads_colorspace(read_clrs_inputs) def reset_frame_range_handles(self): """Set frame range to current asset""" if "data" not in self._asset_entity: msg = "Asset {} don't have set any 'data'".format(self._asset) log.warning(msg) nuke.message(msg) return asset_data = self._asset_entity["data"] missing_cols = [] check_cols = ["fps", "frameStart", "frameEnd", "handleStart", "handleEnd"] for col in check_cols: if col not in asset_data: missing_cols.append(col) if len(missing_cols) > 0: missing = ", ".join(missing_cols) msg = "'{}' are not set for asset '{}'!".format( missing, self._asset) log.warning(msg) nuke.message(msg) return # get handles values handle_start = asset_data["handleStart"] handle_end = asset_data["handleEnd"] fps = float(asset_data["fps"]) frame_start_handle = int(asset_data["frameStart"]) - handle_start frame_end_handle = int(asset_data["frameEnd"]) + handle_end self._root_node["lock_range"].setValue(False) self._root_node["fps"].setValue(fps) self._root_node["first_frame"].setValue(frame_start_handle) self._root_node["last_frame"].setValue(frame_end_handle) self._root_node["lock_range"].setValue(True) # update node graph so knobs are updated update_node_graph() frame_range = '{0}-{1}'.format( int(asset_data["frameStart"]), int(asset_data["frameEnd"]) ) for node in nuke.allNodes(filter="Viewer"): node['frame_range'].setValue(frame_range) node['frame_range_lock'].setValue(True) node['frame_range'].setValue(frame_range) node['frame_range_lock'].setValue(True) if not ASSIST: set_node_data( self._root_node, INSTANCE_DATA_KNOB, { "handleStart": int(handle_start), "handleEnd": int(handle_end) } ) else: log.warning( "NukeAssist mode is not allowing " "updating custom knobs..." ) def reset_resolution(self): """Set resolution to project resolution.""" log.info("Resetting resolution") project_name = get_current_project_name() asset_data = self._asset_entity["data"] format_data = { "width": int(asset_data.get( 'resolutionWidth', asset_data.get('resolution_width'))), "height": int(asset_data.get( 'resolutionHeight', asset_data.get('resolution_height'))), "pixel_aspect": asset_data.get( 'pixelAspect', asset_data.get('pixel_aspect', 1)), "name": project_name } if any(x_ for x_ in format_data.values() if x_ is None): msg = ("Missing set shot attributes in DB." "\nContact your supervisor!." "\n\nWidth: `{width}`" "\nHeight: `{height}`" "\nPixel Aspect: `{pixel_aspect}`").format(**format_data) log.error(msg) nuke.message(msg) existing_format = None for format in nuke.formats(): if format_data["name"] == format.name(): existing_format = format break if existing_format: # Enforce existing format to be correct. existing_format.setWidth(format_data["width"]) existing_format.setHeight(format_data["height"]) existing_format.setPixelAspect(format_data["pixel_aspect"]) else: format_string = self.make_format_string(**format_data) log.info("Creating new format: {}".format(format_string)) nuke.addFormat(format_string) nuke.root()["format"].setValue(format_data["name"]) log.info("Format is set.") # update node graph so knobs are updated update_node_graph() def make_format_string(self, **kwargs): if kwargs.get("r"): return ( "{width} " "{height} " "{x} " "{y} " "{r} " "{t} " "{pixel_aspect:.2f} " "{name}".format(**kwargs) ) else: return ( "{width} " "{height} " "{pixel_aspect:.2f} " "{name}".format(**kwargs) ) def set_context_settings(self): # replace reset resolution from avalon core to pype's self.reset_resolution() # replace reset resolution from avalon core to pype's self.reset_frame_range_handles() # add colorspace menu item self.set_colorspace() def set_favorites(self): from .utils import set_context_favorites work_dir = os.getenv("AVALON_WORKDIR") asset = get_current_asset_name() favorite_items = OrderedDict() # project # get project's root and split to parts projects_root = os.path.normpath(work_dir.split( Context.project_name)[0]) # add project name project_dir = os.path.join(projects_root, Context.project_name) + "/" # add to favorites favorite_items.update({"Project dir": project_dir.replace("\\", "/")}) # asset asset_root = os.path.normpath(work_dir.split( asset)[0]) # add asset name asset_dir = os.path.join(asset_root, asset) + "/" # add to favorites favorite_items.update({"Shot dir": asset_dir.replace("\\", "/")}) # workdir favorite_items.update({"Work dir": work_dir.replace("\\", "/")}) set_context_favorites(favorite_items) def get_write_node_template_attr(node): ''' Gets all defined data from presets ''' # TODO: add identifiers to settings and rename settings key plugin_names_mapping = { "create_write_image": "CreateWriteImage", "create_write_prerender": "CreateWritePrerender", "create_write_render": "CreateWriteRender" } # get avalon data from node node_data = get_node_data(node, INSTANCE_DATA_KNOB) identifier = node_data["creator_identifier"] # return template data return get_imageio_node_setting( node_class="Write", plugin_name=plugin_names_mapping[identifier], subset=node_data["subset"] ) def get_dependent_nodes(nodes): """Get all dependent nodes connected to the list of nodes. Looking for connections outside of the nodes in incoming argument. Arguments: nodes (list): list of nuke.Node objects Returns: connections_in: dictionary of nodes and its dependencies connections_out: dictionary of nodes and its dependency """ connections_in = dict() connections_out = dict() node_names = [n.name() for n in nodes] for node in nodes: inputs = node.dependencies() outputs = node.dependent() # collect all inputs outside test_in = [(i, n) for i, n in enumerate(inputs) if n.name() not in node_names] if test_in: connections_in.update({ node: test_in }) # collect all outputs outside test_out = [i for i in outputs if i.name() not in node_names] if test_out: # only one dependent node is allowed connections_out.update({ node: test_out[-1] }) return connections_in, connections_out def update_node_graph(): # Resetting frame will update knob values try: root_node_lock = nuke.root()["lock_range"].value() nuke.root()["lock_range"].setValue(not root_node_lock) nuke.root()["lock_range"].setValue(root_node_lock) current_frame = nuke.frame() nuke.frame(1) nuke.frame(int(current_frame)) except Exception as error: log.warning(error) def find_free_space_to_paste_nodes( nodes, group=nuke.root(), direction="right", offset=300 ): """ For getting coordinates in DAG (node graph) for placing new nodes Arguments: nodes (list): list of nuke.Node objects group (nuke.Node) [optional]: object in which context it is direction (str) [optional]: where we want it to be placed [left, right, top, bottom] offset (int) [optional]: what offset it is from rest of nodes Returns: xpos (int): x coordinace in DAG ypos (int): y coordinace in DAG """ if len(nodes) == 0: return 0, 0 group_xpos = list() group_ypos = list() # get local coordinates of all nodes nodes_xpos = [n.xpos() for n in nodes] + \ [n.xpos() + n.screenWidth() for n in nodes] nodes_ypos = [n.ypos() for n in nodes] + \ [n.ypos() + n.screenHeight() for n in nodes] # get complete screen size of all nodes to be placed in nodes_screen_width = max(nodes_xpos) - min(nodes_xpos) nodes_screen_heigth = max(nodes_ypos) - min(nodes_ypos) # get screen size (r,l,t,b) of all nodes in `group` with group: group_xpos = [n.xpos() for n in nuke.allNodes() if n not in nodes] + \ [n.xpos() + n.screenWidth() for n in nuke.allNodes() if n not in nodes] group_ypos = [n.ypos() for n in nuke.allNodes() if n not in nodes] + \ [n.ypos() + n.screenHeight() for n in nuke.allNodes() if n not in nodes] # calc output left if direction in "left": xpos = min(group_xpos) - abs(nodes_screen_width) - abs(offset) ypos = min(group_ypos) return xpos, ypos # calc output right if direction in "right": xpos = max(group_xpos) + abs(offset) ypos = min(group_ypos) return xpos, ypos # calc output top if direction in "top": xpos = min(group_xpos) ypos = min(group_ypos) - abs(nodes_screen_heigth) - abs(offset) return xpos, ypos # calc output bottom if direction in "bottom": xpos = min(group_xpos) ypos = max(group_ypos) + abs(offset) return xpos, ypos @contextlib.contextmanager def maintained_selection(exclude_nodes=None): """Maintain selection during context Maintain selection during context and unselect all nodes after context is done. Arguments: exclude_nodes (list[nuke.Node]): list of nodes to be unselected before context is done Example: >>> with maintained_selection(): ... node["selected"].setValue(True) >>> print(node["selected"].value()) False """ if exclude_nodes: for node in exclude_nodes: node["selected"].setValue(False) previous_selection = nuke.selectedNodes() try: yield finally: # unselect all selection in case there is some reset_selection() # and select all previously selected nodes if previous_selection: select_nodes(previous_selection) @contextlib.contextmanager def swap_node_with_dependency(old_node, new_node): """ Swap node with dependency Swap node with dependency and reconnect all inputs and outputs. It removes old node. Arguments: old_node (nuke.Node): node to be replaced new_node (nuke.Node): node to replace with Example: >>> old_node_name = old_node["name"].value() >>> print(old_node_name) old_node_name_01 >>> with swap_node_with_dependency(old_node, new_node) as node_name: ... new_node["name"].setValue(node_name) >>> print(new_node["name"].value()) old_node_name_01 """ # preserve position xpos, ypos = old_node.xpos(), old_node.ypos() # preserve selection after all is done outputs = get_node_outputs(old_node) inputs = old_node.dependencies() node_name = old_node["name"].value() try: nuke.delete(old_node) yield node_name finally: # Reconnect inputs for i, node in enumerate(inputs): new_node.setInput(i, node) # Reconnect outputs if outputs: for n, pipes in outputs.items(): for i in pipes: n.setInput(i, new_node) # return to original position new_node.setXYpos(xpos, ypos) def reset_selection(): """Deselect all selected nodes""" for node in nuke.selectedNodes(): node["selected"].setValue(False) def select_nodes(nodes): """Selects all inputted nodes Arguments: nodes (Union[list, tuple, set]): nuke nodes to be selected """ assert isinstance(nodes, (list, tuple, set)), \ "nodes has to be list, tuple or set" for node in nodes: node["selected"].setValue(True) def launch_workfiles_app(): """Show workfiles tool on nuke launch. Trigger to show workfiles tool on application launch. Can be executed only once all other calls are ignored. Workfiles tool show is deferred after application initialization using QTimer. """ if Context.workfiles_launched: return Context.workfiles_launched = True # get all imortant settings open_at_start = env_value_to_bool( env_key="OPENPYPE_WORKFILE_TOOL_ON_START", default=None) # return if none is defined if not open_at_start: return # Show workfiles tool using timer # - this will be probably triggered during initialization in that case # the application is not be able to show uis so it must be # deferred using timer # - timer should be processed when initialization ends # When applications starts to process events. timer = QtCore.QTimer() timer.timeout.connect(_launch_workfile_app) timer.setInterval(100) Context.workfiles_tool_timer = timer timer.start() def _launch_workfile_app(): # Safeguard to not show window when application is still starting up # or is already closing down. closing_down = QtWidgets.QApplication.closingDown() starting_up = QtWidgets.QApplication.startingUp() # Stop the timer if application finished start up of is closing down if closing_down or not starting_up: Context.workfiles_tool_timer.stop() Context.workfiles_tool_timer = None # Skip if application is starting up or closing down if starting_up or closing_down: return # Make sure on top is enabled on first show so the window is not hidden # under main nuke window # - this happened on Centos 7 and it is because the focus of nuke # changes to the main window after showing because of initialization # which moves workfiles tool under it host_tools.show_workfiles(parent=None, on_top=True) @deprecated("openpype.hosts.nuke.api.lib.start_workfile_template_builder") def process_workfile_builder(): """ [DEPRECATED] Process workfile builder on nuke start This function is deprecated and will be removed in future versions. Use settings for `project_settings/nuke/templated_workfile_build` which are supported by api `start_workfile_template_builder()`. """ # to avoid looping of the callback, remove it! nuke.removeOnCreate(process_workfile_builder, nodeClass="Root") # get state from settings project_settings = get_current_project_settings() workfile_builder = project_settings["nuke"].get( "workfile_builder", {}) # get settings create_fv_on = workfile_builder.get("create_first_version") or None builder_on = workfile_builder.get("builder_on_start") or None last_workfile_path = os.environ.get("AVALON_LAST_WORKFILE") # generate first version in file not existing and feature is enabled if create_fv_on and not os.path.exists(last_workfile_path): # get custom template path if any custom_template_path = get_custom_workfile_template_from_session( project_settings=project_settings ) # if custom template is defined if custom_template_path: log.info("Adding nodes from `{}`...".format( custom_template_path )) try: # import nodes into current script nuke.nodePaste(custom_template_path) except RuntimeError: raise RuntimeError(( "Template defined for project: {} is not working. " "Talk to your manager for an advise").format( custom_template_path)) # if builder at start is defined if builder_on: log.info("Building nodes from presets...") # build nodes by defined presets BuildWorkfile().process() log.info("Saving script as version `{}`...".format( last_workfile_path )) # safe file as version save_file(last_workfile_path) return def start_workfile_template_builder(): from .workfile_template_builder import ( build_workfile_template ) # remove callback since it would be duplicating the workfile nuke.removeOnCreate(start_workfile_template_builder, nodeClass="Root") # to avoid looping of the callback, remove it! log.info("Starting workfile template builder...") try: build_workfile_template(workfile_creation_enabled=True) except TemplateProfileNotFound: log.warning("Template profile not found. Skipping...") @deprecated def recreate_instance(origin_node, avalon_data=None): """Recreate input instance to different data Args: origin_node (nuke.Node): Nuke node to be recreating from avalon_data (dict, optional): data to be used in new node avalon_data Returns: nuke.Node: newly created node """ knobs_wl = ["render", "publish", "review", "ypos", "use_limit", "first", "last"] # get data from avalon knobs data = get_avalon_knob_data( origin_node) # add input data to avalon data if avalon_data: data.update(avalon_data) # capture all node knobs allowed in op_knobs knobs_data = {k: origin_node[k].value() for k in origin_node.knobs() for key in knobs_wl if key in k} # get node dependencies inputs = origin_node.dependencies() outputs = origin_node.dependent() # remove the node nuke.delete(origin_node) # create new node # get appropriate plugin class creator_plugin = None for Creator in discover_legacy_creator_plugins(): if Creator.__name__ == data["creator"]: creator_plugin = Creator break # create write node with creator new_node_name = data["subset"] new_node = creator_plugin(new_node_name, data["asset"]).process() # white listed knobs to the new node for _k, _v in knobs_data.items(): try: print(_k, _v) new_node[_k].setValue(_v) except Exception as e: print(e) # connect to original inputs for i, n in enumerate(inputs): new_node.setInput(i, n) # connect to outputs if len(outputs) > 0: for dn in outputs: dn.setInput(0, new_node) return new_node def add_scripts_menu(): try: from scriptsmenu import launchfornuke except ImportError: log.warning( "Skipping studio.menu install, because " "'scriptsmenu' module seems unavailable." ) return # load configuration of custom menu project_name = get_current_project_name() project_settings = get_project_settings(project_name) config = project_settings["nuke"]["scriptsmenu"]["definition"] _menu = project_settings["nuke"]["scriptsmenu"]["name"] if not config: log.warning("Skipping studio menu, no definition found.") return # run the launcher for Maya menu studio_menu = launchfornuke.main(title=_menu.title()) # apply configuration studio_menu.build_from_configuration(studio_menu, config) def add_scripts_gizmo(): # load configuration of custom menu project_name = get_current_project_name() project_settings = get_project_settings(project_name) platform_name = platform.system().lower() for gizmo_settings in project_settings["nuke"]["gizmo"]: gizmo_list_definition = gizmo_settings["gizmo_definition"] toolbar_name = gizmo_settings["toolbar_menu_name"] # gizmo_toolbar_path = gizmo_settings["gizmo_toolbar_path"] gizmo_source_dir = gizmo_settings.get( "gizmo_source_dir", {}).get(platform_name) toolbar_icon_path = gizmo_settings.get( "toolbar_icon_path", {}).get(platform_name) if not gizmo_source_dir: log.debug("Skipping studio gizmo `{}`, " "no gizmo path found.".format(toolbar_name) ) return if not gizmo_list_definition: log.debug("Skipping studio gizmo `{}`, " "no definition found.".format(toolbar_name) ) return if toolbar_icon_path: try: toolbar_icon_path = toolbar_icon_path.format(**os.environ) except KeyError as e: log.error( "This environment variable doesn't exist: {}".format(e) ) existing_gizmo_path = [] for source_dir in gizmo_source_dir: try: resolve_source_dir = source_dir.format(**os.environ) except KeyError as e: log.error( "This environment variable doesn't exist: {}".format(e) ) continue if not os.path.exists(resolve_source_dir): log.warning( "The source of gizmo `{}` does not exists".format( resolve_source_dir ) ) continue existing_gizmo_path.append(resolve_source_dir) # run the launcher for Nuke toolbar toolbar_menu = gizmo_menu.GizmoMenu( title=toolbar_name, icon=toolbar_icon_path ) # apply configuration toolbar_menu.add_gizmo_path(existing_gizmo_path) toolbar_menu.build_from_configuration(gizmo_list_definition) class NukeDirmap(HostDirmap): def __init__(self, file_name, *args, **kwargs): """ Args: file_name (str): full path of referenced file from workfiles *args (tuple): Positional arguments for 'HostDirmap' class **kwargs (dict): Keyword arguments for 'HostDirmap' class """ self.file_name = file_name super(NukeDirmap, self).__init__(*args, **kwargs) def on_enable_dirmap(self): pass def dirmap_routine(self, source_path, destination_path): source_path = source_path.lower().replace(os.sep, '/') destination_path = destination_path.lower().replace(os.sep, '/') log.debug("Map: {} with: {}->{}".format(self.file_name, source_path, destination_path)) if platform.system().lower() == "windows": self.file_name = self.file_name.lower().replace( source_path, destination_path) else: self.file_name = self.file_name.replace( source_path, destination_path) class DirmapCache: """Caching class to get settings and sync_module easily and only once.""" _project_name = None _project_settings = None _sync_module_discovered = False _sync_module = None _mapping = None @classmethod def project_name(cls): if cls._project_name is None: cls._project_name = os.getenv("AVALON_PROJECT") return cls._project_name @classmethod def project_settings(cls): if cls._project_settings is None: cls._project_settings = get_project_settings(cls.project_name()) return cls._project_settings @classmethod def sync_module(cls): if not cls._sync_module_discovered: cls._sync_module_discovered = True cls._sync_module = ModulesManager().modules_by_name.get( "sync_server") return cls._sync_module @classmethod def mapping(cls): return cls._mapping @classmethod def set_mapping(cls, mapping): cls._mapping = mapping def dirmap_file_name_filter(file_name): """Nuke callback function with single full path argument. Checks project settings for potential mapping from source to dest. """ dirmap_processor = NukeDirmap( file_name, "nuke", DirmapCache.project_name(), DirmapCache.project_settings(), DirmapCache.sync_module(), ) if not DirmapCache.mapping(): DirmapCache.set_mapping(dirmap_processor.get_mappings()) dirmap_processor.process_dirmap(DirmapCache.mapping()) if os.path.exists(dirmap_processor.file_name): return dirmap_processor.file_name return file_name @contextlib.contextmanager def node_tempfile(): """Create a temp file where node is pasted during duplication. This is to avoid using clipboard for node duplication. """ tmp_file = tempfile.NamedTemporaryFile( mode="w", prefix="openpype_nuke_temp_", suffix=".nk", delete=False ) tmp_file.close() node_tempfile_path = tmp_file.name try: # Yield the path where node can be copied yield node_tempfile_path finally: # Remove the file at the end os.remove(node_tempfile_path) def duplicate_node(node): reset_selection() # select required node for duplication node.setSelected(True) with node_tempfile() as filepath: # copy selected to temp filepath nuke.nodeCopy(filepath) # reset selection reset_selection() # paste node and selection is on it only dupli_node = nuke.nodePaste(filepath) # reset selection reset_selection() return dupli_node def get_group_io_nodes(nodes): """Get the input and the output of a group of nodes.""" if not nodes: raise ValueError("there is no nodes in the list") input_node = None output_node = None if len(nodes) == 1: input_node = output_node = nodes[0] else: for node in nodes: if "Input" in node.name(): input_node = node if "Output" in node.name(): output_node = node if input_node is not None and output_node is not None: break if input_node is None: log.warning("No Input found") if output_node is None: log.warning("No Output found") return input_node, output_node def get_extreme_positions(nodes): """Get the 4 numbers that represent the box of a group of nodes.""" if not nodes: raise ValueError("there is no nodes in the list") nodes_xpos = [n.xpos() for n in nodes] + \ [n.xpos() + n.screenWidth() for n in nodes] nodes_ypos = [n.ypos() for n in nodes] + \ [n.ypos() + n.screenHeight() for n in nodes] min_x, min_y = (min(nodes_xpos), min(nodes_ypos)) max_x, max_y = (max(nodes_xpos), max(nodes_ypos)) return min_x, min_y, max_x, max_y def refresh_node(node): """Correct a bug caused by the multi-threading of nuke. Refresh the node to make sure that it takes the desired attributes. """ x = node.xpos() y = node.ypos() nuke.autoplaceSnap(node) node.setXYpos(x, y) def refresh_nodes(nodes): for node in nodes: refresh_node(node) def get_names_from_nodes(nodes): """Get list of nodes names. Args: nodes(List[nuke.Node]): List of nodes to convert into names. Returns: List[str]: Name of passed nodes. """ return [ node.name() for node in nodes ] def get_nodes_by_names(names): """Get list of nuke nodes based on their names. Args: names (List[str]): List of node names to be found. Returns: List[nuke.Node]: List of nodes found by name. """ return [ nuke.toNode(name) for name in names ] def get_viewer_config_from_string(input_string): """Convert string to display and viewer string Args: input_string (str): string with viewer Raises: IndexError: if more then one slash in input string IndexError: if missing closing bracket Returns: tuple[str]: display, viewer """ display = None viewer = input_string # check if () or / or \ in name if "/" in viewer: split = viewer.split("/") # rise if more then one column if len(split) > 2: raise IndexError(( "Viewer Input string is not correct. " "more then two `/` slashes! {}" ).format(input_string)) viewer = split[1] display = split[0] elif "(" in viewer: pattern = r"([\w\d\s\.\-]+).*[(](.*)[)]" result_ = re.findall(pattern, viewer) try: result_ = result_.pop() display = str(result_[1]).rstrip() viewer = str(result_[0]).rstrip() except IndexError: raise IndexError(( "Viewer Input string is not correct. " "Missing bracket! {}" ).format(input_string)) return (display, viewer) def create_viewer_profile_string(viewer, display=None, path_like=False): """Convert viewer and display to string Args: viewer (str): viewer name display (Optional[str]): display name path_like (Optional[bool]): if True, return path like string Returns: str: viewer config string """ if not display: return viewer if path_like: return "{}/{}".format(display, viewer) return "{} ({})".format(viewer, display) def get_filenames_without_hash(filename, frame_start, frame_end): """Get filenames without frame hash i.e. "renderCompositingMain.baking.0001.exr" Args: filename (str): filename with frame hash frame_start (str): start of the frame frame_end (str): end of the frame Returns: list: filename per frame of the sequence """ filenames = [] for frame in range(int(frame_start), (int(frame_end) + 1)): if "#" in filename: # use regex to convert #### to {:0>4} def replace(match): return "{{:0>{}}}".format(len(match.group())) filename_without_hashes = re.sub("#+", replace, filename) new_filename = filename_without_hashes.format(frame) filenames.append(new_filename) return filenames def create_camera_node_by_version(): """Function to create the camera with the latest node class For Nuke version 14.0 or later, the Camera4 camera node class would be used For the version before, the Camera2 camera node class would be used Returns: Node: camera node """ nuke_number_version = nuke.NUKE_VERSION_MAJOR if nuke_number_version >= 14: return nuke.createNode("Camera4") else: return nuke.createNode("Camera2") def link_knobs(knobs, node, group_node): """Link knobs from inside `group_node`""" missing_knobs = [] for knob in knobs: if knob in group_node.knobs(): continue if knob not in node.knobs().keys(): missing_knobs.append(knob) link = nuke.Link_Knob("") link.makeLink(node.name(), knob) link.setName(knob) link.setFlag(0x1000) group_node.addKnob(link) if missing_knobs: raise ValueError( "Write node exposed knobs missing:\n\n{}\n\nPlease review" " project settings.".format("\n".join(missing_knobs)) ) ================================================ FILE: openpype/hosts/nuke/api/pipeline.py ================================================ import nuke import os import importlib from collections import OrderedDict, defaultdict import pyblish.api import openpype from openpype.host import ( HostBase, IWorkfileHost, ILoadHost, IPublishHost ) from openpype.settings import get_current_project_settings from openpype.lib import register_event_callback, Logger from openpype.pipeline import ( register_loader_plugin_path, register_creator_plugin_path, register_inventory_action_path, AVALON_CONTAINER_ID, get_current_asset_name, get_current_task_name, ) from openpype.pipeline.workfile import BuildWorkfile from openpype.tools.utils import host_tools from .command import viewer_update_and_undo_stop from .lib import ( Context, ROOT_DATA_KNOB, INSTANCE_DATA_KNOB, get_main_window, add_publish_knob, WorkfileSettings, # TODO: remove this once workfile builder will be removed process_workfile_builder, start_workfile_template_builder, launch_workfiles_app, check_inventory_versions, set_avalon_knob_data, read_avalon_data, on_script_load, dirmap_file_name_filter, add_scripts_menu, add_scripts_gizmo, get_node_data, set_node_data ) from .workfile_template_builder import ( NukePlaceholderLoadPlugin, NukePlaceholderCreatePlugin, build_workfile_template, create_placeholder, update_placeholder, ) from .workio import ( open_file, save_file, file_extensions, has_unsaved_changes, work_root, current_file ) from .constants import ASSIST from . import push_to_project log = Logger.get_logger(__name__) HOST_DIR = os.path.dirname(os.path.abspath(openpype.hosts.nuke.__file__)) PLUGINS_DIR = os.path.join(HOST_DIR, "plugins") PUBLISH_PATH = os.path.join(PLUGINS_DIR, "publish") LOAD_PATH = os.path.join(PLUGINS_DIR, "load") CREATE_PATH = os.path.join(PLUGINS_DIR, "create") INVENTORY_PATH = os.path.join(PLUGINS_DIR, "inventory") MENU_LABEL = os.environ["AVALON_LABEL"] # registering pyblish gui regarding settings in presets if os.getenv("PYBLISH_GUI", None): pyblish.api.register_gui(os.getenv("PYBLISH_GUI", None)) class NukeHost( HostBase, IWorkfileHost, ILoadHost, IPublishHost ): name = "nuke" def open_workfile(self, filepath): return open_file(filepath) def save_workfile(self, filepath=None): return save_file(filepath) def work_root(self, session): return work_root(session) def get_current_workfile(self): return current_file() def workfile_has_unsaved_changes(self): return has_unsaved_changes() def get_workfile_extensions(self): return file_extensions() def get_workfile_build_placeholder_plugins(self): return [ NukePlaceholderLoadPlugin, NukePlaceholderCreatePlugin ] def get_containers(self): return ls() def install(self): ''' Installing all requarements for Nuke host ''' pyblish.api.register_host("nuke") self.log.info("Registering Nuke plug-ins..") pyblish.api.register_plugin_path(PUBLISH_PATH) register_loader_plugin_path(LOAD_PATH) register_creator_plugin_path(CREATE_PATH) register_inventory_action_path(INVENTORY_PATH) # Register Avalon event for workfiles loading. register_event_callback("workio.open_file", check_inventory_versions) register_event_callback("taskChanged", change_context_label) _install_menu() # add script menu add_scripts_menu() add_scripts_gizmo() add_nuke_callbacks() launch_workfiles_app() def get_context_data(self): root_node = nuke.root() return get_node_data(root_node, ROOT_DATA_KNOB) def update_context_data(self, data, changes): root_node = nuke.root() set_node_data(root_node, ROOT_DATA_KNOB, data) def add_nuke_callbacks(): """ Adding all available nuke callbacks """ nuke_settings = get_current_project_settings()["nuke"] workfile_settings = WorkfileSettings() # Set context settings. nuke.addOnCreate( workfile_settings.set_context_settings, nodeClass="Root") # adding favorites to file browser nuke.addOnCreate(workfile_settings.set_favorites, nodeClass="Root") # template builder callbacks nuke.addOnCreate(start_workfile_template_builder, nodeClass="Root") # TODO: remove this callback once workfile builder will be removed nuke.addOnCreate(process_workfile_builder, nodeClass="Root") # fix ffmpeg settings on script nuke.addOnScriptLoad(on_script_load) # set checker for last versions on loaded containers nuke.addOnScriptLoad(check_inventory_versions) nuke.addOnScriptSave(check_inventory_versions) # set apply all workfile settings on script load and save nuke.addOnScriptLoad(WorkfileSettings().set_context_settings) if nuke_settings["nuke-dirmap"]["enabled"]: log.info("Added Nuke's dir-mapping callback ...") # Add dirmap for file paths. nuke.addFilenameFilter(dirmap_file_name_filter) log.info("Added Nuke callbacks ...") def reload_config(): """Attempt to reload pipeline at run-time. CAUTION: This is primarily for development and debugging purposes. """ for module in ( "openpype.hosts.nuke.api.actions", "openpype.hosts.nuke.api.menu", "openpype.hosts.nuke.api.plugin", "openpype.hosts.nuke.api.lib", ): log.info("Reloading module: {}...".format(module)) module = importlib.import_module(module) try: importlib.reload(module) except AttributeError as e: from importlib import reload log.warning("Cannot reload module: {}".format(e)) reload(module) def _show_workfiles(): # Make sure parent is not set # - this makes Workfiles tool as separated window which # avoid issues with reopening # - it is possible to explicitly change on top flag of the tool host_tools.show_workfiles(parent=None, on_top=False) def get_context_label(): return "{0}, {1}".format( get_current_asset_name(), get_current_task_name() ) def _install_menu(): """Install Avalon menu into Nuke's main menu bar.""" # uninstall original avalon menu main_window = get_main_window() menubar = nuke.menu("Nuke") menu = menubar.addMenu(MENU_LABEL) if not ASSIST: label = get_context_label() context_action_item = menu.addCommand("Context") context_action_item.setEnabled(False) Context.context_action_item = context_action_item context_action = context_action_item.action() context_action.setText(label) # add separator after context label menu.addSeparator() menu.addCommand( "Work Files...", _show_workfiles ) menu.addSeparator() if not ASSIST: # only add parent if nuke version is 14 or higher # known issue with no solution yet menu.addCommand( "Create...", lambda: host_tools.show_publisher( parent=main_window, tab="create" ) ) # only add parent if nuke version is 14 or higher # known issue with no solution yet menu.addCommand( "Publish...", lambda: host_tools.show_publisher( parent=main_window, tab="publish" ) ) menu.addCommand( "Load...", lambda: host_tools.show_loader( parent=main_window, use_context=True ) ) menu.addCommand( "Manage...", lambda: host_tools.show_scene_inventory(parent=main_window) ) menu.addSeparator() menu.addCommand( "Library...", lambda: host_tools.show_library_loader( parent=main_window ) ) menu.addSeparator() menu.addCommand( "Set Resolution", lambda: WorkfileSettings().reset_resolution() ) menu.addCommand( "Set Frame Range", lambda: WorkfileSettings().reset_frame_range_handles() ) menu.addCommand( "Set Colorspace", lambda: WorkfileSettings().set_colorspace() ) menu.addCommand( "Apply All Settings", lambda: WorkfileSettings().set_context_settings() ) menu.addSeparator() menu.addCommand( "Build Workfile", lambda: BuildWorkfile().process() ) menu_template = menu.addMenu("Template Builder") # creating template menu menu_template.addCommand( "Build Workfile from template", lambda: build_workfile_template() ) if not ASSIST: menu_template.addSeparator() menu_template.addCommand( "Create Place Holder", lambda: create_placeholder() ) menu_template.addCommand( "Update Place Holder", lambda: update_placeholder() ) menu.addSeparator() menu.addCommand( "Experimental tools...", lambda: host_tools.show_experimental_tools_dialog(parent=main_window) ) menu.addCommand( "Push to Project", lambda: push_to_project.main() ) menu.addSeparator() # add reload pipeline only in debug mode if bool(os.getenv("NUKE_DEBUG")): menu.addSeparator() menu.addCommand("Reload Pipeline", reload_config) # adding shortcuts add_shortcuts_from_presets() def change_context_label(): if ASSIST: return context_action_item = Context.context_action_item if context_action_item is None: return context_action = context_action_item.action() old_label = context_action.text() new_label = get_context_label() context_action.setText(new_label) log.info("Task label changed from `{}` to `{}`".format( old_label, new_label)) def add_shortcuts_from_presets(): menubar = nuke.menu("Nuke") nuke_presets = get_current_project_settings()["nuke"]["general"] if nuke_presets.get("menu"): menu_label_mapping = { "create": "Create...", "manage": "Manage...", "load": "Load...", "build_workfile": "Build Workfile", "publish": "Publish..." } for command_name, shortcut_str in nuke_presets.get("menu").items(): log.info("menu_name `{}` | menu_label `{}`".format( command_name, MENU_LABEL )) log.info("Adding Shortcut `{}` to `{}`".format( shortcut_str, command_name )) try: menu = menubar.findItem(MENU_LABEL) item_label = menu_label_mapping[command_name] menuitem = menu.findItem(item_label) menuitem.setShortcut(shortcut_str) except (AttributeError, KeyError) as e: log.error(e) def containerise(node, name, namespace, context, loader=None, data=None): """Bundle `node` into an assembly and imprint it with metadata Containerisation enables a tracking of version, author and origin for loaded assets. Arguments: node (nuke.Node): Nuke's node object to imprint as container name (str): Name of resulting assembly namespace (str): Namespace under which to host container context (dict): Asset information loader (str, optional): Name of node used to produce this container. Returns: node (nuke.Node): containerised nuke's node object """ data = OrderedDict( [ ("schema", "openpype:container-2.0"), ("id", AVALON_CONTAINER_ID), ("name", name), ("namespace", namespace), ("loader", str(loader)), ("representation", context["representation"]["_id"]), ], **data or dict() ) set_avalon_knob_data(node, data) # set tab to first native node.setTab(0) return node def parse_container(node): """Returns containerised data of a node Reads the imprinted data from `containerise`. Arguments: node (nuke.Node): Nuke's node object to read imprinted data Returns: dict: The container schema data for this container node. """ data = read_avalon_data(node) # If not all required data return the empty container required = ["schema", "id", "name", "namespace", "loader", "representation"] if not all(key in data for key in required): return # Store the node's name data.update({ "objectName": node.fullName(), "node": node, }) return data def update_container(node, keys=None): """Returns node with updateted containder data Arguments: node (nuke.Node): The node in Nuke to imprint as container, keys (dict, optional): data which should be updated Returns: node (nuke.Node): nuke node with updated container data Raises: TypeError on given an invalid container node """ keys = keys or dict() container = parse_container(node) if not container: raise TypeError("Not a valid container node.") container.update(keys) node = set_avalon_knob_data(node, container) return node def ls(): """List available containers. This function is used by the Container Manager in Nuke. You'll need to implement a for-loop that then *yields* one Container at a time. See the `container.json` schema for details on how it should look, and the Maya equivalent, which is in `avalon.maya.pipeline` """ all_nodes = nuke.allNodes(recurseGroups=False) nodes = [n for n in all_nodes] for n in nodes: container = parse_container(n) if container: yield container def list_instances(creator_id=None): """List all created instances to publish from current workfile. For SubsetManager Args: creator_id (Optional[str]): creator identifier Returns: (list) of dictionaries matching instances format """ instances_by_order = defaultdict(list) subset_instances = [] instance_ids = set() for node in nuke.allNodes(recurseGroups=True): if node.Class() in ["Viewer", "Dot"]: continue try: if node["disable"].value(): continue except NameError: # pass if disable knob doesn't exist pass # get data from avalon knob instance_data = get_node_data( node, INSTANCE_DATA_KNOB) if not instance_data: continue if instance_data["id"] != "pyblish.avalon.instance": continue if creator_id and instance_data["creator_identifier"] != creator_id: continue instance_id = instance_data.get("instance_id") if not instance_id: pass elif instance_id in instance_ids: instance_data.pop("instance_id") else: instance_ids.add(instance_id) # node name could change, so update subset name data _update_subset_name_data(instance_data, node) if "render_order" not in node.knobs(): subset_instances.append((node, instance_data)) continue order = int(node["render_order"].value()) instances_by_order[order].append((node, instance_data)) # Sort instances based on order attribute or subset name. # TODO: remove in future Publisher enhanced with sorting ordered_instances = [] for key in sorted(instances_by_order.keys()): instances_by_subset = defaultdict(list) for node, data_ in instances_by_order[key]: instances_by_subset[data_["subset"]].append((node, data_)) for subkey in sorted(instances_by_subset.keys()): ordered_instances.extend(instances_by_subset[subkey]) instances_by_subset = defaultdict(list) for node, data_ in subset_instances: instances_by_subset[data_["subset"]].append((node, data_)) for key in sorted(instances_by_subset.keys()): ordered_instances.extend(instances_by_subset[key]) return ordered_instances def _update_subset_name_data(instance_data, node): """Update subset name data in instance data. Args: instance_data (dict): instance creator data node (nuke.Node): nuke node """ # make sure node name is subset name old_subset_name = instance_data["subset"] old_variant = instance_data["variant"] subset_name_root = old_subset_name.replace(old_variant, "") new_subset_name = node.name() new_variant = new_subset_name.replace(subset_name_root, "") instance_data["subset"] = new_subset_name instance_data["variant"] = new_variant def remove_instance(instance): """Remove instance from current workfile metadata. For SubsetManager Args: instance (dict): instance representation from subsetmanager model """ instance_node = instance.transient_data["node"] instance_knob = instance_node.knobs()[INSTANCE_DATA_KNOB] instance_node.removeKnob(instance_knob) nuke.delete(instance_node) def select_instance(instance): """ Select instance in Node View Args: instance (dict): instance representation from subsetmanager model """ instance_node = instance.transient_data["node"] instance_node["selected"].setValue(True) ================================================ FILE: openpype/hosts/nuke/api/plugin.py ================================================ import nuke import re import os import sys import six import random import string from collections import OrderedDict, defaultdict from abc import abstractmethod from openpype.settings import get_current_project_settings from openpype.lib import ( BoolDef, EnumDef ) from openpype.pipeline import ( LegacyCreator, LoaderPlugin, CreatorError, Creator as NewCreator, CreatedInstance, get_current_task_name ) from openpype.pipeline.colorspace import ( get_display_view_colorspace_name, get_colorspace_settings_from_publish_context, set_colorspace_data_to_representation ) from openpype.lib.transcoding import ( VIDEO_EXTENSIONS ) from .lib import ( INSTANCE_DATA_KNOB, Knobby, check_subsetname_exists, maintained_selection, get_avalon_knob_data, set_avalon_knob_data, add_publish_knob, get_nuke_imageio_settings, set_node_knobs_from_settings, set_node_data, get_node_data, get_view_process_node, get_viewer_config_from_string, deprecated, get_filenames_without_hash, link_knobs ) from .pipeline import ( list_instances, remove_instance ) def _collect_and_cache_nodes(creator): key = "openpype.nuke.nodes" if key not in creator.collection_shared_data: instances_by_identifier = defaultdict(list) for item in list_instances(): _, instance_data = item identifier = instance_data["creator_identifier"] instances_by_identifier[identifier].append(item) creator.collection_shared_data[key] = instances_by_identifier return creator.collection_shared_data[key] class NukeCreatorError(CreatorError): pass class NukeCreator(NewCreator): selected_nodes = [] def pass_pre_attributes_to_instance( self, instance_data, pre_create_data, keys=None ): if not keys: keys = pre_create_data.keys() creator_attrs = instance_data["creator_attributes"] = {} for pass_key in keys: creator_attrs[pass_key] = pre_create_data[pass_key] def check_existing_subset(self, subset_name): """Make sure subset name is unique. It search within all nodes recursively and checks if subset name is found in any node having instance data knob. Arguments: subset_name (str): Subset name """ for node in nuke.allNodes(recurseGroups=True): # make sure testing node is having instance knob if INSTANCE_DATA_KNOB not in node.knobs().keys(): continue node_data = get_node_data(node, INSTANCE_DATA_KNOB) if not node_data: # a node has no instance data continue # test if subset name is matching if node_data.get("subset") == subset_name: raise NukeCreatorError( ( "A publish instance for '{}' already exists " "in nodes! Please change the variant " "name to ensure unique output." ).format(subset_name) ) def create_instance_node( self, node_name, knobs=None, parent=None, node_type=None ): """Create node representing instance. Arguments: node_name (str): Name of the new node. knobs (OrderedDict): node knobs name and values parent (str): Name of the parent node. node_type (str, optional): Nuke node Class. Returns: nuke.Node: Newly created instance node. """ node_type = node_type or "NoOp" node_knobs = knobs or {} # set parent node parent_node = nuke.root() if parent: parent_node = nuke.toNode(parent) try: with parent_node: created_node = nuke.createNode(node_type) created_node["name"].setValue(node_name) for key, values in node_knobs.items(): if key in created_node.knobs(): created_node["key"].setValue(values) except Exception as _err: raise NukeCreatorError("Creating have failed: {}".format(_err)) return created_node def set_selected_nodes(self, pre_create_data): if pre_create_data.get("use_selection"): self.selected_nodes = nuke.selectedNodes() if self.selected_nodes == []: raise NukeCreatorError("Creator error: No active selection") else: self.selected_nodes = [] def create(self, subset_name, instance_data, pre_create_data): # make sure selected nodes are added self.set_selected_nodes(pre_create_data) # make sure subset name is unique self.check_existing_subset(subset_name) try: instance_node = self.create_instance_node( subset_name, node_type=instance_data.pop("node_type", None) ) instance = CreatedInstance( self.family, subset_name, instance_data, self ) instance.transient_data["node"] = instance_node self._add_instance_to_context(instance) set_node_data( instance_node, INSTANCE_DATA_KNOB, instance.data_to_store()) return instance except Exception as er: six.reraise( NukeCreatorError, NukeCreatorError("Creator error: {}".format(er)), sys.exc_info()[2]) def collect_instances(self): cached_instances = _collect_and_cache_nodes(self) attr_def_keys = { attr_def.key for attr_def in self.get_instance_attr_defs() } attr_def_keys.discard(None) for (node, data) in cached_instances[self.identifier]: created_instance = CreatedInstance.from_existing( data, self ) created_instance.transient_data["node"] = node self._add_instance_to_context(created_instance) for key in ( set(created_instance["creator_attributes"].keys()) - attr_def_keys ): created_instance["creator_attributes"].pop(key) def update_instances(self, update_list): for created_inst, changes in update_list: instance_node = created_inst.transient_data["node"] # update instance node name if subset name changed if "subset" in changes.changed_keys: instance_node["name"].setValue( changes["subset"].new_value ) # in case node is not existing anymore (user erased it manually) try: instance_node.fullName() except ValueError: self.remove_instances([created_inst]) continue set_node_data( instance_node, INSTANCE_DATA_KNOB, created_inst.data_to_store() ) def remove_instances(self, instances): for instance in instances: remove_instance(instance) self._remove_instance_from_context(instance) def get_pre_create_attr_defs(self): return [ BoolDef( "use_selection", default=not self.create_context.headless, label="Use selection" ) ] def get_creator_settings(self, project_settings, settings_key=None): if not settings_key: settings_key = self.__class__.__name__ return project_settings["nuke"]["create"][settings_key] class NukeWriteCreator(NukeCreator): """Add Publishable Write node""" identifier = "create_write" label = "Create Write" family = "write" icon = "sign-out" def get_linked_knobs(self): linked_knobs = [] if "channels" in self.instance_attributes: linked_knobs.append("channels") if "ordered" in self.instance_attributes: linked_knobs.append("render_order") if "use_range_limit" in self.instance_attributes: linked_knobs.extend(["___", "first", "last", "use_limit"]) return linked_knobs def integrate_links(self, node, outputs=True): # skip if no selection if not self.selected_node: return # collect dependencies input_nodes = [self.selected_node] dependent_nodes = self.selected_node.dependent() if outputs else [] # relinking to collected connections for i, input in enumerate(input_nodes): node.setInput(i, input) # make it nicer in graph node.autoplace() # relink also dependent nodes for dep_nodes in dependent_nodes: dep_nodes.setInput(0, node) def set_selected_nodes(self, pre_create_data): if pre_create_data.get("use_selection"): selected_nodes = nuke.selectedNodes() if selected_nodes == []: raise NukeCreatorError("Creator error: No active selection") elif len(selected_nodes) > 1: NukeCreatorError("Creator error: Select only one camera node") self.selected_node = selected_nodes[0] else: self.selected_node = None def get_pre_create_attr_defs(self): attr_defs = [ BoolDef("use_selection", label="Use selection"), self._get_render_target_enum() ] return attr_defs def get_instance_attr_defs(self): attr_defs = [ self._get_render_target_enum(), ] # add reviewable attribute if "reviewable" in self.instance_attributes: attr_defs.append(self._get_reviewable_bool()) return attr_defs def _get_render_target_enum(self): rendering_targets = { "local": "Local machine rendering", "frames": "Use existing frames" } if ("farm_rendering" in self.instance_attributes): rendering_targets["frames_farm"] = "Use existing frames - farm" rendering_targets["farm"] = "Farm rendering" return EnumDef( "render_target", items=rendering_targets, label="Render target" ) def _get_reviewable_bool(self): return BoolDef( "review", default=True, label="Review" ) def create(self, subset_name, instance_data, pre_create_data): # make sure selected nodes are added self.set_selected_nodes(pre_create_data) # make sure subset name is unique self.check_existing_subset(subset_name) instance_node = self.create_instance_node( subset_name, instance_data ) try: instance = CreatedInstance( self.family, subset_name, instance_data, self ) instance.transient_data["node"] = instance_node self._add_instance_to_context(instance) set_node_data( instance_node, INSTANCE_DATA_KNOB, instance.data_to_store()) return instance except Exception as er: six.reraise( NukeCreatorError, NukeCreatorError("Creator error: {}".format(er)), sys.exc_info()[2] ) def apply_settings(self, project_settings): """Method called on initialization of plugin to apply settings.""" # plugin settings plugin_settings = self.get_creator_settings(project_settings) # individual attributes self.instance_attributes = plugin_settings.get( "instance_attributes") or self.instance_attributes self.prenodes = plugin_settings["prenodes"] self.default_variants = plugin_settings.get( "default_variants") or self.default_variants self.temp_rendering_path_template = ( plugin_settings.get("temp_rendering_path_template") or self.temp_rendering_path_template ) class OpenPypeCreator(LegacyCreator): """Pype Nuke Creator class wrapper""" node_color = "0xdfea5dff" def __init__(self, *args, **kwargs): super(OpenPypeCreator, self).__init__(*args, **kwargs) if check_subsetname_exists( nuke.allNodes(), self.data["subset"]): msg = ("The subset name `{0}` is already used on a node in" "this workfile.".format(self.data["subset"])) self.log.error(msg + "\n\nPlease use other subset name!") raise NameError("`{0}: {1}".format(__name__, msg)) return def process(self): from nukescripts import autoBackdrop instance = None if (self.options or {}).get("useSelection"): nodes = nuke.selectedNodes() if not nodes: nuke.message("Please select nodes that you " "wish to add to a container") return elif len(nodes) == 1: # only one node is selected instance = nodes[0] if not instance: # Not using selection or multiple nodes selected bckd_node = autoBackdrop() bckd_node["tile_color"].setValue(int(self.node_color, 16)) bckd_node["note_font_size"].setValue(24) bckd_node["label"].setValue("[{}]".format(self.name)) instance = bckd_node # add avalon knobs set_avalon_knob_data(instance, self.data) add_publish_knob(instance) return instance def get_instance_group_node_childs(instance): """Return list of instance group node children Args: instance (pyblish.Instance): pyblish instance Returns: list: [nuke.Node] """ node = instance.data["transientData"]["node"] if node.Class() != "Group": return # collect child nodes child_nodes = [] # iterate all nodes for node in nuke.allNodes(group=node): # add contained nodes to instance's node list child_nodes.append(node) return child_nodes def get_colorspace_from_node(node): # Add version data to instance colorspace = node["colorspace"].value() # remove default part of the string if "default (" in colorspace: colorspace = re.sub(r"default.\(|\)", "", colorspace) return colorspace def get_review_presets_config(): settings = get_current_project_settings() review_profiles = ( settings["global"] ["publish"] ["ExtractReview"] ["profiles"] ) outputs = {} for profile in review_profiles: outputs.update(profile.get("outputs", {})) return [str(name) for name, _prop in outputs.items()] class NukeLoader(LoaderPlugin): container_id_knob = "containerId" container_id = None def reset_container_id(self): self.container_id = "".join(random.choice( string.ascii_uppercase + string.digits) for _ in range(10)) def get_container_id(self, node): id_knob = node.knobs().get(self.container_id_knob) return id_knob.value() if id_knob else None def get_members(self, source): """Return nodes that has same "containerId" as `source`""" source_id = self.get_container_id(source) return [node for node in nuke.allNodes(recurseGroups=True) if self.get_container_id(node) == source_id and node is not source] if source_id else [] def set_as_member(self, node): source_id = self.get_container_id(node) if source_id: node[self.container_id_knob].setValue(source_id) else: HIDEN_FLAG = 0x00040000 _knob = Knobby( "String_Knob", self.container_id, flags=[ nuke.READ_ONLY, HIDEN_FLAG ]) knob = _knob.create(self.container_id_knob) node.addKnob(knob) def clear_members(self, parent_node): parent_class = parent_node.Class() members = self.get_members(parent_node) dependent_nodes = None for node in members: _depndc = [n for n in node.dependent() if n not in members] if not _depndc: continue dependent_nodes = _depndc break for member in members: if member.Class() == parent_class: continue self.log.info("removing node: `{}".format(member.name())) nuke.delete(member) return dependent_nodes class ExporterReview(object): """ Base class object for generating review data from Nuke Args: klass (pyblish.plugin): pyblish plugin parent instance (pyblish.instance): instance of pyblish context """ data = None publish_on_farm = False def __init__(self, klass, instance, multiple_presets=True ): self.log = klass.log self.instance = instance self.multiple_presets = multiple_presets self.path_in = self.instance.data.get("path", None) self.staging_dir = self.instance.data["stagingDir"] self.collection = self.instance.data.get("collection", None) self.data = {"representations": []} def get_file_info(self): if self.collection: # get path self.fname = os.path.basename( self.collection.format("{head}{padding}{tail}") ) self.fhead = self.collection.format("{head}") # get first and last frame self.first_frame = min(self.collection.indexes) self.last_frame = max(self.collection.indexes) # make sure slate frame is not included frame_start_handle = self.instance.data["frameStartHandle"] if frame_start_handle > self.first_frame: self.first_frame = frame_start_handle else: self.fname = os.path.basename(self.path_in) self.fhead = os.path.splitext(self.fname)[0] + "." self.first_frame = self.instance.data["frameStartHandle"] self.last_frame = self.instance.data["frameEndHandle"] if "#" in self.fhead: self.fhead = self.fhead.replace("#", "")[:-1] def get_representation_data( self, tags=None, range=False, custom_tags=None, colorspace=None ): """ Add representation data to self.data Args: tags (list[str], optional): list of defined tags. Defaults to None. range (bool, optional): flag for adding ranges. Defaults to False. custom_tags (list[str], optional): user inputted custom tags. Defaults to None. """ add_tags = tags or [] repre = { "name": self.name, "ext": self.ext, "files": self.file, "stagingDir": self.staging_dir, "tags": [self.name.replace("_", "-")] + add_tags } if custom_tags: repre["custom_tags"] = custom_tags if range: repre.update({ "frameStart": self.first_frame, "frameEnd": self.last_frame, }) if ".{}".format(self.ext) not in VIDEO_EXTENSIONS: filenames = get_filenames_without_hash( self.file, self.first_frame, self.last_frame) repre["files"] = filenames if self.multiple_presets: repre["outputName"] = self.name if self.publish_on_farm: repre["tags"].append("publish_on_farm") # add colorspace data to representation if colorspace: set_colorspace_data_to_representation( repre, self.instance.context.data, colorspace=colorspace, log=self.log ) self.data["representations"].append(repre) def get_imageio_baking_profile(self): from . import lib as opnlib nuke_imageio = opnlib.get_nuke_imageio_settings() # TODO: this is only securing backward compatibility lets remove # this once all projects's anatomy are updated to newer config if "baking" in nuke_imageio.keys(): return nuke_imageio["baking"]["viewerProcess"] else: return nuke_imageio["viewer"]["viewerProcess"] class ExporterReviewLut(ExporterReview): """ Generator object for review lut from Nuke Args: klass (pyblish.plugin): pyblish plugin parent instance (pyblish.instance): instance of pyblish context """ _temp_nodes = [] def __init__(self, klass, instance, name=None, ext=None, cube_size=None, lut_size=None, lut_style=None, multiple_presets=True): # initialize parent class super(ExporterReviewLut, self).__init__( klass, instance, multiple_presets) # deal with now lut defined in viewer lut if hasattr(klass, "viewer_lut_raw"): self.viewer_lut_raw = klass.viewer_lut_raw else: self.viewer_lut_raw = False self.name = name or "baked_lut" self.ext = ext or "cube" self.cube_size = cube_size or 32 self.lut_size = lut_size or 1024 self.lut_style = lut_style or "linear" # set frame start / end and file name to self self.get_file_info() self.log.info("File info was set...") self.file = self.fhead + self.name + ".{}".format(self.ext) self.path = os.path.join( self.staging_dir, self.file).replace("\\", "/") def clean_nodes(self): for node in self._temp_nodes: nuke.delete(node) self._temp_nodes = [] self.log.info("Deleted nodes...") def generate_lut(self, **kwargs): bake_viewer_process = kwargs["bake_viewer_process"] bake_viewer_input_process_node = kwargs[ "bake_viewer_input_process"] # ---------- start nodes creation # CMSTestPattern cms_node = nuke.createNode("CMSTestPattern") cms_node["cube_size"].setValue(self.cube_size) # connect self._temp_nodes.append(cms_node) self.previous_node = cms_node if bake_viewer_process: # Node View Process if bake_viewer_input_process_node: ipn = get_view_process_node() if ipn is not None: # connect ipn.setInput(0, self.previous_node) self._temp_nodes.append(ipn) self.previous_node = ipn self.log.debug( "ViewProcess... `{}`".format(self._temp_nodes)) if not self.viewer_lut_raw: # OCIODisplay dag_node = nuke.createNode("OCIODisplay") # connect dag_node.setInput(0, self.previous_node) self._temp_nodes.append(dag_node) self.previous_node = dag_node self.log.debug( "OCIODisplay... `{}`".format(self._temp_nodes)) # GenerateLUT gen_lut_node = nuke.createNode("GenerateLUT") gen_lut_node["file"].setValue(self.path) gen_lut_node["file_type"].setValue(".{}".format(self.ext)) gen_lut_node["lut1d"].setValue(self.lut_size) gen_lut_node["style1d"].setValue(self.lut_style) # connect gen_lut_node.setInput(0, self.previous_node) self._temp_nodes.append(gen_lut_node) # ---------- end nodes creation # Export lut file nuke.execute( gen_lut_node.name(), int(self.first_frame), int(self.first_frame)) self.log.info("Exported...") # ---------- generate representation data self.get_representation_data() # ---------- Clean up self.clean_nodes() return self.data class ExporterReviewMov(ExporterReview): """ Metaclass for generating review mov files Args: klass (pyblish.plugin): pyblish plugin parent instance (pyblish.instance): instance of pyblish context """ _temp_nodes = {} def __init__(self, klass, instance, name=None, ext=None, multiple_presets=True ): # initialize parent class super(ExporterReviewMov, self).__init__( klass, instance, multiple_presets) # passing presets for nodes to self self.nodes = klass.nodes if hasattr(klass, "nodes") else {} # deal with now lut defined in viewer lut self.viewer_lut_raw = klass.viewer_lut_raw self.write_colorspace = instance.data["colorspace"] self.name = name or "baked" self.ext = ext or "mov" # set frame start / end and file name to self self.get_file_info() self.log.info("File info was set...") if ".{}".format(self.ext) in VIDEO_EXTENSIONS: self.file = "{}{}.{}".format( self.fhead, self.name, self.ext) else: # Output is image (or image sequence) # When the file is an image it's possible it # has extra information after the `fhead` that # we want to preserve, e.g. like frame numbers # or frames hashes like `####` filename_no_ext = os.path.splitext( os.path.basename(self.path_in))[0] after_head = filename_no_ext[len(self.fhead):] self.file = "{}{}.{}.{}".format( self.fhead, self.name, after_head, self.ext) self.path = os.path.join( self.staging_dir, self.file).replace("\\", "/") def clean_nodes(self, node_name): for node in self._temp_nodes[node_name]: nuke.delete(node) self._temp_nodes[node_name] = [] self.log.info("Deleted nodes...") def render(self, render_node_name): self.log.info("Rendering... ") # Render Write node nuke.execute( render_node_name, int(self.first_frame), int(self.last_frame)) self.log.info("Rendered...") def save_file(self): import shutil with maintained_selection(): self.log.info("Saving nodes as file... ") # create nk path path = os.path.splitext(self.path)[0] + ".nk" # save file to the path if not os.path.exists(os.path.dirname(path)): os.makedirs(os.path.dirname(path)) shutil.copyfile(self.instance.context.data["currentFile"], path) self.log.info("Nodes exported...") return path def generate_mov(self, farm=False, **kwargs): # colorspace data colorspace = None # get colorspace settings # get colorspace data from context config_data, _ = get_colorspace_settings_from_publish_context( self.instance.context.data) add_tags = [] self.publish_on_farm = farm read_raw = kwargs["read_raw"] bake_viewer_process = kwargs["bake_viewer_process"] bake_viewer_input_process_node = kwargs[ "bake_viewer_input_process"] viewer_process_override = kwargs[ "viewer_process_override"] baking_view_profile = ( viewer_process_override or self.get_imageio_baking_profile()) fps = self.instance.context.data["fps"] self.log.debug(">> baking_view_profile `{}`".format( baking_view_profile)) add_custom_tags = kwargs.get("add_custom_tags", []) self.log.info( "__ add_custom_tags: `{0}`".format(add_custom_tags)) subset = self.instance.data["subset"] self._temp_nodes[subset] = [] # Read node r_node = nuke.createNode("Read") r_node["file"].setValue(self.path_in) r_node["first"].setValue(self.first_frame) r_node["origfirst"].setValue(self.first_frame) r_node["last"].setValue(self.last_frame) r_node["origlast"].setValue(self.last_frame) r_node["colorspace"].setValue(self.write_colorspace) # do not rely on defaults, set explicitly # to be sure it is set correctly r_node["frame_mode"].setValue("expression") r_node["frame"].setValue("") if read_raw: r_node["raw"].setValue(1) # connect to Read node self._shift_to_previous_node_and_temp(subset, r_node, "Read... `{}`") # add reformat node reformat_nodes_config = kwargs["reformat_nodes_config"] if reformat_nodes_config["enabled"]: reposition_nodes = reformat_nodes_config["reposition_nodes"] for reposition_node in reposition_nodes: node_class = reposition_node["node_class"] knobs = reposition_node["knobs"] node = nuke.createNode(node_class) set_node_knobs_from_settings(node, knobs) # connect in order self._connect_to_above_nodes( node, subset, "Reposition node... `{}`" ) # append reformated tag add_tags.append("reformated") # only create colorspace baking if toggled on if bake_viewer_process: if bake_viewer_input_process_node: # View Process node ipn = get_view_process_node() if ipn is not None: # connect to ViewProcess node self._connect_to_above_nodes(ipn, subset, "ViewProcess... `{}`") if not self.viewer_lut_raw: # OCIODisplay dag_node = nuke.createNode("OCIODisplay") # assign display display, viewer = get_viewer_config_from_string( str(baking_view_profile) ) if display: dag_node["display"].setValue(display) # assign viewer dag_node["view"].setValue(viewer) if config_data: # convert display and view to colorspace colorspace = get_display_view_colorspace_name( config_path=config_data["path"], display=display, view=viewer ) self._connect_to_above_nodes(dag_node, subset, "OCIODisplay... `{}`") # Write node write_node = nuke.createNode("Write") self.log.debug("Path: {}".format(self.path)) write_node["file"].setValue(str(self.path)) write_node["file_type"].setValue(str(self.ext)) # Knobs `meta_codec` and `mov64_codec` are not available on centos. # TODO shouldn't this come from settings on outputs? try: write_node["meta_codec"].setValue("ap4h") except Exception: self.log.info("`meta_codec` knob was not found") try: write_node["mov64_codec"].setValue("ap4h") write_node["mov64_fps"].setValue(float(fps)) except Exception: self.log.info("`mov64_codec` knob was not found") try: write_node["mov64_write_timecode"].setValue(1) except Exception: self.log.info("`mov64_write_timecode` knob was not found") write_node["raw"].setValue(1) # connect write_node.setInput(0, self.previous_node) self._temp_nodes[subset].append(write_node) self.log.debug("Write... `{}`".format(self._temp_nodes[subset])) # ---------- end nodes creation # ---------- render or save to nk if self.publish_on_farm: nuke.scriptSave() path_nk = self.save_file() self.data.update({ "bakeScriptPath": path_nk, "bakeWriteNodeName": write_node.name(), "bakeRenderPath": self.path }) else: self.render(write_node.name()) # ---------- generate representation data self.get_representation_data( tags=["review", "need_thumbnail", "delete"] + add_tags, custom_tags=add_custom_tags, range=True, colorspace=colorspace ) self.log.debug("Representation... `{}`".format(self.data)) self.clean_nodes(subset) nuke.scriptSave() return self.data def _shift_to_previous_node_and_temp(self, subset, node, message): self._temp_nodes[subset].append(node) self.previous_node = node self.log.debug(message.format(self._temp_nodes[subset])) def _connect_to_above_nodes(self, node, subset, message): node.setInput(0, self.previous_node) self._shift_to_previous_node_and_temp(subset, node, message) @deprecated("openpype.hosts.nuke.api.plugin.NukeWriteCreator") class AbstractWriteRender(OpenPypeCreator): """Abstract creator to gather similar implementation for Write creators""" name = "" label = "" hosts = ["nuke"] n_class = "Write" family = "render" icon = "sign-out" defaults = ["Main", "Mask"] knobs = [] prenodes = {} def __init__(self, *args, **kwargs): super(AbstractWriteRender, self).__init__(*args, **kwargs) data = OrderedDict() data["family"] = self.family data["families"] = self.n_class for k, v in self.data.items(): if k not in data.keys(): data.update({k: v}) self.data = data self.nodes = nuke.selectedNodes() def process(self): inputs = [] outputs = [] instance = nuke.toNode(self.data["subset"]) selected_node = None # use selection if (self.options or {}).get("useSelection"): nodes = self.nodes if not (len(nodes) < 2): msg = ("Select only one node. " "The node you want to connect to, " "or tick off `Use selection`") self.log.error(msg) nuke.message(msg) return if len(nodes) == 0: msg = ( "No nodes selected. Please select a single node to connect" " to or tick off `Use selection`" ) self.log.error(msg) nuke.message(msg) return selected_node = nodes[0] inputs = [selected_node] outputs = selected_node.dependent() if instance: if (instance.name() in selected_node.name()): selected_node = instance.dependencies()[0] # if node already exist if instance: # collect input / outputs inputs = instance.dependencies() outputs = instance.dependent() selected_node = inputs[0] # remove old one nuke.delete(instance) # recreate new write_data = { "nodeclass": self.n_class, "families": [self.family], "avalon": self.data, "subset": self.data["subset"], "knobs": self.knobs } # add creator data creator_data = {"creator": self.__class__.__name__} self.data.update(creator_data) write_data.update(creator_data) write_node = self._create_write_node( selected_node, inputs, outputs, write_data ) # relinking to collected connections for i, input in enumerate(inputs): write_node.setInput(i, input) write_node.autoplace() for output in outputs: output.setInput(0, write_node) write_node = self._modify_write_node(write_node) return write_node def is_legacy(self): """Check if it needs to run legacy code In case where `type` key is missing in single knob it is legacy project anatomy. Returns: bool: True if legacy """ imageio_nodes = get_nuke_imageio_settings()["nodes"] node = imageio_nodes["requiredNodes"][0] if "type" not in node["knobs"][0]: # if type is not yet in project anatomy return True elif next(iter( _k for _k in node["knobs"] if _k.get("type") == "__legacy__" ), None): # in case someone re-saved anatomy # with old configuration return True @abstractmethod def _create_write_node(self, selected_node, inputs, outputs, write_data): """Family dependent implementation of Write node creation Args: selected_node (nuke.Node) inputs (list of nuke.Node) - input dependencies (what is connected) outputs (list of nuke.Node) - output dependencies write_data (dict) - values used to fill Knobs Returns: node (nuke.Node): group node with data as Knobs """ pass @abstractmethod def _modify_write_node(self, write_node): """Family dependent modification of created 'write_node' Returns: node (nuke.Node): group node with data as Knobs """ pass def convert_to_valid_instaces(): """ Check and convert to latest publisher instances Also save as new minor version of workfile. """ def family_to_identifier(family): mapping = { "render": "create_write_render", "prerender": "create_write_prerender", "still": "create_write_image", "model": "create_model", "camera": "create_camera", "nukenodes": "create_backdrop", "gizmo": "create_gizmo", "source": "create_source" } return mapping[family] from openpype.hosts.nuke.api import workio task_name = get_current_task_name() # save into new workfile current_file = workio.current_file() # add file suffex if not if "_publisherConvert" not in current_file: new_workfile = ( current_file[:-3] + "_publisherConvert" + current_file[-3:] ) else: new_workfile = current_file path = new_workfile.replace("\\", "/") nuke.scriptSaveAs(new_workfile, overwrite=1) nuke.Root()["name"].setValue(path) nuke.Root()["project_directory"].setValue(os.path.dirname(path)) nuke.Root().setModified(False) _remove_old_knobs(nuke.Root()) # loop all nodes and convert for node in nuke.allNodes(recurseGroups=True): transfer_data = { "creator_attributes": {} } creator_attr = transfer_data["creator_attributes"] if node.Class() in ["Viewer", "Dot"]: continue if get_node_data(node, INSTANCE_DATA_KNOB): continue # get data from avalon knob avalon_knob_data = get_avalon_knob_data( node, ["avalon:", "ak:"]) if not avalon_knob_data: continue if avalon_knob_data["id"] != "pyblish.avalon.instance": continue transfer_data.update({ k: v for k, v in avalon_knob_data.items() if k not in ["families", "creator"] }) transfer_data["task"] = task_name family = avalon_knob_data["family"] # establish families families_ak = avalon_knob_data.get("families", []) if "suspend_publish" in node.knobs(): creator_attr["suspended_publish"] = ( node["suspend_publish"].value()) # get review knob value if "review" in node.knobs(): creator_attr["review"] = ( node["review"].value()) if "publish" in node.knobs(): transfer_data["active"] = ( node["publish"].value()) # add idetifier transfer_data["creator_identifier"] = family_to_identifier(family) # Add all nodes in group instances. if node.Class() == "Group": # only alter families for render family if families_ak and "write" in families_ak.lower(): target = node["render"].value() if target == "Use existing frames": creator_attr["render_target"] = "frames" elif target == "Local": # Local rendering creator_attr["render_target"] = "local" elif target == "On farm": # Farm rendering creator_attr["render_target"] = "farm" if "deadlinePriority" in node.knobs(): transfer_data["farm_priority"] = ( node["deadlinePriority"].value()) if "deadlineChunkSize" in node.knobs(): creator_attr["farm_chunk"] = ( node["deadlineChunkSize"].value()) if "deadlineConcurrentTasks" in node.knobs(): creator_attr["farm_concurrency"] = ( node["deadlineConcurrentTasks"].value()) _remove_old_knobs(node) # add new instance knob with transfer data set_node_data( node, INSTANCE_DATA_KNOB, transfer_data) nuke.scriptSave() def _remove_old_knobs(node): remove_knobs = [ "review", "publish", "render", "suspend_publish", "warn", "divd", "OpenpypeDataGroup", "OpenpypeDataGroup_End", "deadlinePriority", "deadlineChunkSize", "deadlineConcurrentTasks", "Deadline" ] print(node.name()) # remove all old knobs for knob in node.allKnobs(): try: if knob.name() in remove_knobs: node.removeKnob(knob) elif "avalon" in knob.name(): node.removeKnob(knob) except ValueError: pass def exposed_write_knobs(settings, plugin_name, instance_node): exposed_knobs = settings["nuke"]["create"][plugin_name].get( "exposed_knobs", [] ) if exposed_knobs: instance_node.addKnob(nuke.Text_Knob('', 'Write Knobs')) write_node = nuke.allNodes(group=instance_node, filter="Write")[0] link_knobs(exposed_knobs, write_node, instance_node) ================================================ FILE: openpype/hosts/nuke/api/push_to_project.py ================================================ from collections import defaultdict import shutil import os from openpype.client import get_project, get_asset_by_id from openpype.settings import get_system_settings, get_project_settings from openpype.pipeline import Anatomy, registered_host from openpype.pipeline.template_data import get_template_data from openpype.pipeline.workfile import get_workdir_with_workdir_data from openpype.tools.push_to_project.app import show from .utils import bake_gizmos_recursively import nuke def bake_container(container): """Bake containers to read nodes.""" node = container["node"] # Fetch knobs to remove in order. knobs_to_remove = [] remove = False for count in range(0, node.numKnobs()): knob = node.knob(count) # All knobs from "OpenPype" tab knob onwards. if knob.name() == "OpenPype": remove = True if remove: knobs_to_remove.append(knob) # Dont remove knobs from "containerId" onwards. if knob.name() == "containerId": remove = False # Knobs needs to be remove in reverse order, because child knobs needs to # be remove first. for knob in reversed(knobs_to_remove): node.removeKnob(knob) node["tile_color"].setValue(0) def main(): context = show("", "", False, True) if context is None: return # Get workfile path to save to. project_name = context["project_name"] project_doc = get_project(project_name) asset_doc = get_asset_by_id(project_name, context["asset_id"]) task_name = context["task_name"] host = registered_host() system_settings = get_system_settings() project_settings = get_project_settings(project_name) anatomy = Anatomy(project_name) workdir_data = get_template_data( project_doc, asset_doc, task_name, host.name, system_settings ) workdir = get_workdir_with_workdir_data( workdir_data, project_name, anatomy, project_settings=project_settings ) # Save current workfile. current_file = host.current_file() host.save_file(current_file) for container in host.ls(): bake_container(container) # Bake gizmos. bake_gizmos_recursively() # Copy all read node files to "resources" folder next to workfile and # change file path. first_frame = int(nuke.root()["first_frame"].value()) last_frame = int(nuke.root()["last_frame"].value()) files_by_node_name = defaultdict(set) nodes_by_name = {} for count in range(first_frame, last_frame + 1): nuke.frame(count) for node in nuke.allNodes(filter="Read"): files_by_node_name[node.name()].add( nuke.filename(node, nuke.REPLACE) ) nodes_by_name[node.name()] = node resources_dir = os.path.join(workdir, "resources") for name, files in files_by_node_name.items(): dir = os.path.join(resources_dir, name) if not os.path.exists(dir): os.makedirs(dir) for f in files: shutil.copy(f, os.path.join(dir, os.path.basename(f))) node = nodes_by_name[name] path = node["file"].value().replace(os.path.dirname(f), dir) node["file"].setValue(path.replace("\\", "/")) # Save current workfile to new context. basename = os.path.basename(current_file) host.save_file(os.path.join(workdir, basename)) # Open current contex workfile. host.open_file(current_file) ================================================ FILE: openpype/hosts/nuke/api/utils.py ================================================ import os import re import nuke from openpype import resources from qtpy import QtWidgets def set_context_favorites(favorites=None): """ Adding favorite folders to nuke's browser Arguments: favorites (dict): couples of {name:path} """ favorites = favorites or {} icon_path = resources.get_resource("icons", "folder-favorite.png") for name, path in favorites.items(): nuke.addFavoriteDir( name, path, nuke.IMAGE | nuke.SCRIPT | nuke.GEO, icon=icon_path) def get_node_outputs(node): ''' Return a dictionary of the nodes and pipes that are connected to node ''' dep_dict = {} dependencies = node.dependent(nuke.INPUTS | nuke.HIDDEN_INPUTS) for d in dependencies: dep_dict[d] = [] for i in range(d.inputs()): if d.input(i) == node: dep_dict[d].append(i) return dep_dict def is_node_gizmo(node): ''' return True if node is gizmo ''' return 'gizmo_file' in node.knobs() def gizmo_is_nuke_default(gizmo): '''Check if gizmo is in default install path''' plug_dir = os.path.join(os.path.dirname( nuke.env['ExecutablePath']), 'plugins') return gizmo.filename().startswith(plug_dir) def bake_gizmos_recursively(in_group=None): """Converting a gizmo to group Arguments: is_group (nuke.Node)[optonal]: group node or all nodes """ from .lib import maintained_selection if in_group is None: in_group = nuke.Root() # preserve selection after all is done with maintained_selection(): # jump to the group with in_group: for node in nuke.allNodes(): if is_node_gizmo(node) and not gizmo_is_nuke_default(node): with node: outputs = get_node_outputs(node) group = node.makeGroup() # Reconnect inputs and outputs if any if outputs: for n, pipes in outputs.items(): for i in pipes: n.setInput(i, group) for i in range(node.inputs()): group.setInput(i, node.input(i)) # set node position and name group.setXYpos(node.xpos(), node.ypos()) name = node.name() nuke.delete(node) group.setName(name) node = group if node.Class() == "Group": bake_gizmos_recursively(node) def colorspace_exists_on_node(node, colorspace_name): """ Check if colorspace exists on node Look through all options in the colorspace knob, and see if we have an exact match to one of the items. Args: node (nuke.Node): nuke node object colorspace_name (str): color profile name Returns: bool: True if exists """ try: colorspace_knob = node['colorspace'] except ValueError: # knob is not available on input node return False return colorspace_name in get_colorspace_list(colorspace_knob) def get_colorspace_list(colorspace_knob): """Get available colorspace profile names Args: colorspace_knob (nuke.Knob): nuke knob object Returns: list: list of strings names of profiles """ results = [] # This pattern is to match with roles which uses an indentation and # parentheses with original colorspace. The value returned from the # colorspace is the string before the indentation, so we'll need to # convert the values to match with value returned from the knob, # ei. knob.value(). pattern = r".*\t.* \(.*\)" for colorspace in nuke.getColorspaceList(colorspace_knob): match = re.search(pattern, colorspace) if match: results.append(colorspace.split("\t", 1)[0]) else: results.append(colorspace) return results def is_headless(): """ Returns: bool: headless """ return QtWidgets.QApplication.instance() is None ================================================ FILE: openpype/hosts/nuke/api/workfile_template_builder.py ================================================ import collections import nuke from openpype.pipeline import registered_host from openpype.pipeline.workfile.workfile_template_builder import ( AbstractTemplateBuilder, PlaceholderPlugin, LoadPlaceholderItem, CreatePlaceholderItem, PlaceholderLoadMixin, PlaceholderCreateMixin ) from openpype.tools.workfile_template_build import ( WorkfileBuildPlaceholderDialog, ) from .lib import ( find_free_space_to_paste_nodes, get_extreme_positions, get_group_io_nodes, imprint, refresh_node, refresh_nodes, reset_selection, get_names_from_nodes, get_nodes_by_names, select_nodes, duplicate_node, node_tempfile, get_main_window, WorkfileSettings, ) PLACEHOLDER_SET = "PLACEHOLDERS_SET" class NukeTemplateBuilder(AbstractTemplateBuilder): """Concrete implementation of AbstractTemplateBuilder for nuke""" def import_template(self, path): """Import template into current scene. Block if a template is already loaded. Args: path (str): A path to current template (usually given by get_template_preset implementation) Returns: bool: Whether the template was successfully imported or not """ # TODO check if the template is already imported nuke.nodePaste(path) reset_selection() return True class NukePlaceholderPlugin(PlaceholderPlugin): node_color = 4278190335 def _collect_scene_placeholders(self): # Cache placeholder data to shared data placeholder_nodes = self.builder.get_shared_populate_data( "placeholder_nodes" ) if placeholder_nodes is None: placeholder_nodes = {} all_groups = collections.deque() all_groups.append(nuke.thisGroup()) while all_groups: group = all_groups.popleft() for node in group.nodes(): if isinstance(node, nuke.Group): all_groups.append(node) node_knobs = node.knobs() if ( "is_placeholder" not in node_knobs or not node.knob("is_placeholder").value() ): continue if "empty" in node_knobs and node.knob("empty").value(): continue placeholder_nodes[node.fullName()] = node self.builder.set_shared_populate_data( "placeholder_nodes", placeholder_nodes ) return placeholder_nodes def create_placeholder(self, placeholder_data): placeholder_data["plugin_identifier"] = self.identifier placeholder = nuke.nodes.NoOp() placeholder.setName("PLACEHOLDER") placeholder.knob("tile_color").setValue(self.node_color) imprint(placeholder, placeholder_data) imprint(placeholder, {"is_placeholder": True}) placeholder.knob("is_placeholder").setVisible(False) def update_placeholder(self, placeholder_item, placeholder_data): node = nuke.toNode(placeholder_item.scene_identifier) imprint(node, placeholder_data) def _parse_placeholder_node_data(self, node): placeholder_data = {} for key in self.get_placeholder_keys(): knob = node.knob(key) value = None if knob is not None: value = knob.getValue() placeholder_data[key] = value return placeholder_data def delete_placeholder(self, placeholder): """Remove placeholder if building was successful""" placeholder_node = nuke.toNode(placeholder.scene_identifier) nuke.delete(placeholder_node) class NukePlaceholderLoadPlugin(NukePlaceholderPlugin, PlaceholderLoadMixin): identifier = "nuke.load" label = "Nuke load" def _parse_placeholder_node_data(self, node): placeholder_data = super( NukePlaceholderLoadPlugin, self )._parse_placeholder_node_data(node) node_knobs = node.knobs() nb_children = 0 if "nb_children" in node_knobs: nb_children = int(node_knobs["nb_children"].getValue()) placeholder_data["nb_children"] = nb_children siblings = [] if "siblings" in node_knobs: siblings = node_knobs["siblings"].values() placeholder_data["siblings"] = siblings node_full_name = node.fullName() placeholder_data["group_name"] = node_full_name.rpartition(".")[0] placeholder_data["last_loaded"] = [] placeholder_data["delete"] = False return placeholder_data def _get_loaded_repre_ids(self): loaded_representation_ids = self.builder.get_shared_populate_data( "loaded_representation_ids" ) if loaded_representation_ids is None: loaded_representation_ids = set() for node in nuke.allNodes(): if "repre_id" in node.knobs(): loaded_representation_ids.add( node.knob("repre_id").getValue() ) self.builder.set_shared_populate_data( "loaded_representation_ids", loaded_representation_ids ) return loaded_representation_ids def _before_placeholder_load(self, placeholder): placeholder.data["nodes_init"] = nuke.allNodes() def _before_repre_load(self, placeholder, representation): placeholder.data["last_repre_id"] = str(representation["_id"]) def collect_placeholders(self): output = [] scene_placeholders = self._collect_scene_placeholders() for node_name, node in scene_placeholders.items(): plugin_identifier_knob = node.knob("plugin_identifier") if ( plugin_identifier_knob is None or plugin_identifier_knob.getValue() != self.identifier ): continue placeholder_data = self._parse_placeholder_node_data(node) # TODO do data validations and maybe updgrades if are invalid output.append( LoadPlaceholderItem(node_name, placeholder_data, self) ) return output def populate_placeholder(self, placeholder): self.populate_load_placeholder(placeholder) def repopulate_placeholder(self, placeholder): repre_ids = self._get_loaded_repre_ids() self.populate_load_placeholder(placeholder, repre_ids) def get_placeholder_options(self, options=None): return self.get_load_plugin_options(options) def post_placeholder_process(self, placeholder, failed): """Cleanup placeholder after load of its corresponding representations. Args: placeholder (PlaceholderItem): Item which was just used to load representation. failed (bool): Loading of representation failed. """ # deselect all selected nodes placeholder_node = nuke.toNode(placeholder.scene_identifier) # getting the latest nodes added # TODO get from shared populate data! nodes_init = placeholder.data["nodes_init"] nodes_loaded = list(set(nuke.allNodes()) - set(nodes_init)) self.log.debug("Loaded nodes: {}".format(nodes_loaded)) if not nodes_loaded: return placeholder.data["delete"] = True nodes_loaded = self._move_to_placeholder_group( placeholder, nodes_loaded ) placeholder.data["last_loaded"] = nodes_loaded refresh_nodes(nodes_loaded) # positioning of the loaded nodes min_x, min_y, _, _ = get_extreme_positions(nodes_loaded) for node in nodes_loaded: xpos = (node.xpos() - min_x) + placeholder_node.xpos() ypos = (node.ypos() - min_y) + placeholder_node.ypos() node.setXYpos(xpos, ypos) refresh_nodes(nodes_loaded) # fix the problem of z_order for backdrops self._fix_z_order(placeholder) if placeholder.data.get("keep_placeholder"): self._imprint_siblings(placeholder) if placeholder.data["nb_children"] == 0: # save initial nodes positions and dimensions, update them # and set inputs and outputs of loaded nodes if placeholder.data.get("keep_placeholder"): self._imprint_inits() self._update_nodes(placeholder, nuke.allNodes(), nodes_loaded) self._set_loaded_connections(placeholder) elif placeholder.data["siblings"]: # create copies of placeholder siblings for the new loaded nodes, # set their inputs and outputs and update all nodes positions and # dimensions and siblings names siblings = get_nodes_by_names(placeholder.data["siblings"]) refresh_nodes(siblings) copies = self._create_sib_copies(placeholder) new_nodes = list(copies.values()) # copies nodes self._update_nodes(new_nodes, nodes_loaded) placeholder_node.removeKnob(placeholder_node.knob("siblings")) new_nodes_name = get_names_from_nodes(new_nodes) imprint(placeholder_node, {"siblings": new_nodes_name}) self._set_copies_connections(placeholder, copies) self._update_nodes( nuke.allNodes(), new_nodes + nodes_loaded, 20 ) new_siblings = get_names_from_nodes(new_nodes) placeholder.data["siblings"] = new_siblings else: # if the placeholder doesn't have siblings, the loaded # nodes will be placed in a free space xpointer, ypointer = find_free_space_to_paste_nodes( nodes_loaded, direction="bottom", offset=200 ) node = nuke.createNode("NoOp") reset_selection() nuke.delete(node) for node in nodes_loaded: xpos = (node.xpos() - min_x) + xpointer ypos = (node.ypos() - min_y) + ypointer node.setXYpos(xpos, ypos) placeholder.data["nb_children"] += 1 reset_selection() # go back to root group nuke.root().begin() def _move_to_placeholder_group(self, placeholder, nodes_loaded): """ opening the placeholder's group and copying loaded nodes in it. Returns : nodes_loaded (list): the new list of pasted nodes """ groups_name = placeholder.data["group_name"] reset_selection() select_nodes(nodes_loaded) if groups_name: with node_tempfile() as filepath: nuke.nodeCopy(filepath) for node in nuke.selectedNodes(): nuke.delete(node) group = nuke.toNode(groups_name) group.begin() nuke.nodePaste(filepath) nodes_loaded = nuke.selectedNodes() return nodes_loaded def _fix_z_order(self, placeholder): """Fix the problem of z_order when a backdrop is loaded.""" nodes_loaded = placeholder.data["last_loaded"] loaded_backdrops = [] bd_orders = set() for node in nodes_loaded: if isinstance(node, nuke.BackdropNode): loaded_backdrops.append(node) bd_orders.add(node.knob("z_order").getValue()) if not bd_orders: return sib_orders = set() for node_name in placeholder.data["siblings"]: node = nuke.toNode(node_name) if isinstance(node, nuke.BackdropNode): sib_orders.add(node.knob("z_order").getValue()) if not sib_orders: return min_order = min(bd_orders) max_order = max(sib_orders) for backdrop_node in loaded_backdrops: z_order = backdrop_node.knob("z_order").getValue() backdrop_node.knob("z_order").setValue( z_order + max_order - min_order + 1) def _imprint_siblings(self, placeholder): """ - add siblings names to placeholder attributes (nodes loaded with it) - add Id to the attributes of all the other nodes """ loaded_nodes = placeholder.data["last_loaded"] loaded_nodes_set = set(loaded_nodes) data = {"repre_id": str(placeholder.data["last_repre_id"])} for node in loaded_nodes: node_knobs = node.knobs() if "builder_type" not in node_knobs: # save the id of representation for all imported nodes imprint(node, data) node.knob("repre_id").setVisible(False) refresh_node(node) continue if ( "is_placeholder" not in node_knobs or ( "is_placeholder" in node_knobs and node.knob("is_placeholder").value() ) ): siblings = list(loaded_nodes_set - {node}) siblings_name = get_names_from_nodes(siblings) siblings = {"siblings": siblings_name} imprint(node, siblings) def _imprint_inits(self): """Add initial positions and dimensions to the attributes""" for node in nuke.allNodes(): refresh_node(node) imprint(node, {"x_init": node.xpos(), "y_init": node.ypos()}) node.knob("x_init").setVisible(False) node.knob("y_init").setVisible(False) width = node.screenWidth() height = node.screenHeight() if "bdwidth" in node.knobs(): imprint(node, {"w_init": width, "h_init": height}) node.knob("w_init").setVisible(False) node.knob("h_init").setVisible(False) refresh_node(node) def _update_nodes( self, placeholder, nodes, considered_nodes, offset_y=None ): """Adjust backdrop nodes dimensions and positions. Considering some nodes sizes. Args: nodes (list): list of nodes to update considered_nodes (list): list of nodes to consider while updating positions and dimensions offset (int): distance between copies """ placeholder_node = nuke.toNode(placeholder.scene_identifier) min_x, min_y, max_x, max_y = get_extreme_positions(considered_nodes) diff_x = diff_y = 0 contained_nodes = [] # for backdrops if offset_y is None: width_ph = placeholder_node.screenWidth() height_ph = placeholder_node.screenHeight() diff_y = max_y - min_y - height_ph diff_x = max_x - min_x - width_ph contained_nodes = [placeholder_node] min_x = placeholder_node.xpos() min_y = placeholder_node.ypos() else: siblings = get_nodes_by_names(placeholder.data["siblings"]) minX, _, maxX, _ = get_extreme_positions(siblings) diff_y = max_y - min_y + 20 diff_x = abs(max_x - min_x - maxX + minX) contained_nodes = considered_nodes if diff_y <= 0 and diff_x <= 0: return for node in nodes: refresh_node(node) if ( node == placeholder_node or node in considered_nodes ): continue if ( not isinstance(node, nuke.BackdropNode) or ( isinstance(node, nuke.BackdropNode) and not set(contained_nodes) <= set(node.getNodes()) ) ): if offset_y is None and node.xpos() >= min_x: node.setXpos(node.xpos() + diff_x) if node.ypos() >= min_y: node.setYpos(node.ypos() + diff_y) else: width = node.screenWidth() height = node.screenHeight() node.knob("bdwidth").setValue(width + diff_x) node.knob("bdheight").setValue(height + diff_y) refresh_node(node) def _set_loaded_connections(self, placeholder): """ set inputs and outputs of loaded nodes""" placeholder_node = nuke.toNode(placeholder.scene_identifier) input_node, output_node = get_group_io_nodes( placeholder.data["last_loaded"] ) for node in placeholder_node.dependent(): for idx in range(node.inputs()): if node.input(idx) == placeholder_node and output_node: node.setInput(idx, output_node) for node in placeholder_node.dependencies(): for idx in range(placeholder_node.inputs()): if placeholder_node.input(idx) == node and input_node: input_node.setInput(0, node) def _create_sib_copies(self, placeholder): """ creating copies of the palce_holder siblings (the ones who were loaded with it) for the new nodes added Returns : copies (dict) : with copied nodes names and their copies """ copies = {} siblings = get_nodes_by_names(placeholder.data["siblings"]) for node in siblings: new_node = duplicate_node(node) x_init = int(new_node.knob("x_init").getValue()) y_init = int(new_node.knob("y_init").getValue()) new_node.setXYpos(x_init, y_init) if isinstance(new_node, nuke.BackdropNode): w_init = new_node.knob("w_init").getValue() h_init = new_node.knob("h_init").getValue() new_node.knob("bdwidth").setValue(w_init) new_node.knob("bdheight").setValue(h_init) refresh_node(node) if "repre_id" in node.knobs().keys(): node.removeKnob(node.knob("repre_id")) copies[node.name()] = new_node return copies def _set_copies_connections(self, placeholder, copies): """Set inputs and outputs of the copies. Args: copies (dict): Copied nodes by their names. """ last_input, last_output = get_group_io_nodes( placeholder.data["last_loaded"] ) siblings = get_nodes_by_names(placeholder.data["siblings"]) siblings_input, siblings_output = get_group_io_nodes(siblings) copy_input = copies[siblings_input.name()] copy_output = copies[siblings_output.name()] for node_init in siblings: if node_init == siblings_output: continue node_copy = copies[node_init.name()] for node in node_init.dependent(): for idx in range(node.inputs()): if node.input(idx) != node_init: continue if node in siblings: copies[node.name()].setInput(idx, node_copy) else: last_input.setInput(0, node_copy) for node in node_init.dependencies(): for idx in range(node_init.inputs()): if node_init.input(idx) != node: continue if node_init == siblings_input: copy_input.setInput(idx, node) elif node in siblings: node_copy.setInput(idx, copies[node.name()]) else: node_copy.setInput(idx, last_output) siblings_input.setInput(0, copy_output) class NukePlaceholderCreatePlugin( NukePlaceholderPlugin, PlaceholderCreateMixin ): identifier = "nuke.create" label = "Nuke create" def _parse_placeholder_node_data(self, node): placeholder_data = super( NukePlaceholderCreatePlugin, self )._parse_placeholder_node_data(node) node_knobs = node.knobs() nb_children = 0 if "nb_children" in node_knobs: nb_children = int(node_knobs["nb_children"].getValue()) placeholder_data["nb_children"] = nb_children siblings = [] if "siblings" in node_knobs: siblings = node_knobs["siblings"].values() placeholder_data["siblings"] = siblings node_full_name = node.fullName() placeholder_data["group_name"] = node_full_name.rpartition(".")[0] placeholder_data["last_loaded"] = [] placeholder_data["delete"] = False return placeholder_data def _before_instance_create(self, placeholder): placeholder.data["nodes_init"] = nuke.allNodes() def collect_placeholders(self): output = [] scene_placeholders = self._collect_scene_placeholders() for node_name, node in scene_placeholders.items(): plugin_identifier_knob = node.knob("plugin_identifier") if ( plugin_identifier_knob is None or plugin_identifier_knob.getValue() != self.identifier ): continue placeholder_data = self._parse_placeholder_node_data(node) output.append( CreatePlaceholderItem(node_name, placeholder_data, self) ) return output def populate_placeholder(self, placeholder): self.populate_create_placeholder(placeholder) def repopulate_placeholder(self, placeholder): self.populate_create_placeholder(placeholder) def get_placeholder_options(self, options=None): return self.get_create_plugin_options(options) def post_placeholder_process(self, placeholder, failed): """Cleanup placeholder after load of its corresponding representations. Args: placeholder (PlaceholderItem): Item which was just used to load representation. failed (bool): Loading of representation failed. """ # deselect all selected nodes placeholder_node = nuke.toNode(placeholder.scene_identifier) # getting the latest nodes added nodes_init = placeholder.data["nodes_init"] nodes_created = list(set(nuke.allNodes()) - set(nodes_init)) self.log.debug("Created nodes: {}".format(nodes_created)) if not nodes_created: return placeholder.data["delete"] = True nodes_created = self._move_to_placeholder_group( placeholder, nodes_created ) placeholder.data["last_created"] = nodes_created refresh_nodes(nodes_created) # positioning of the created nodes min_x, min_y, _, _ = get_extreme_positions(nodes_created) for node in nodes_created: xpos = (node.xpos() - min_x) + placeholder_node.xpos() ypos = (node.ypos() - min_y) + placeholder_node.ypos() node.setXYpos(xpos, ypos) refresh_nodes(nodes_created) # fix the problem of z_order for backdrops self._fix_z_order(placeholder) if placeholder.data.get("keep_placeholder"): self._imprint_siblings(placeholder) if placeholder.data["nb_children"] == 0: # save initial nodes positions and dimensions, update them # and set inputs and outputs of created nodes if placeholder.data.get("keep_placeholder"): self._imprint_inits() self._update_nodes(placeholder, nuke.allNodes(), nodes_created) self._set_created_connections(placeholder) elif placeholder.data["siblings"]: # create copies of placeholder siblings for the new created nodes, # set their inputs and outputs and update all nodes positions and # dimensions and siblings names siblings = get_nodes_by_names(placeholder.data["siblings"]) refresh_nodes(siblings) copies = self._create_sib_copies(placeholder) new_nodes = list(copies.values()) # copies nodes self._update_nodes(new_nodes, nodes_created) placeholder_node.removeKnob(placeholder_node.knob("siblings")) new_nodes_name = get_names_from_nodes(new_nodes) imprint(placeholder_node, {"siblings": new_nodes_name}) self._set_copies_connections(placeholder, copies) self._update_nodes( nuke.allNodes(), new_nodes + nodes_created, 20 ) new_siblings = get_names_from_nodes(new_nodes) placeholder.data["siblings"] = new_siblings else: # if the placeholder doesn't have siblings, the created # nodes will be placed in a free space xpointer, ypointer = find_free_space_to_paste_nodes( nodes_created, direction="bottom", offset=200 ) node = nuke.createNode("NoOp") reset_selection() nuke.delete(node) for node in nodes_created: xpos = (node.xpos() - min_x) + xpointer ypos = (node.ypos() - min_y) + ypointer node.setXYpos(xpos, ypos) placeholder.data["nb_children"] += 1 reset_selection() # go back to root group nuke.root().begin() def _move_to_placeholder_group(self, placeholder, nodes_created): """ opening the placeholder's group and copying created nodes in it. Returns : nodes_created (list): the new list of pasted nodes """ groups_name = placeholder.data["group_name"] reset_selection() select_nodes(nodes_created) if groups_name: with node_tempfile() as filepath: nuke.nodeCopy(filepath) for node in nuke.selectedNodes(): nuke.delete(node) group = nuke.toNode(groups_name) group.begin() nuke.nodePaste(filepath) nodes_created = nuke.selectedNodes() return nodes_created def _fix_z_order(self, placeholder): """Fix the problem of z_order when a backdrop is create.""" nodes_created = placeholder.data["last_created"] created_backdrops = [] bd_orders = set() for node in nodes_created: if isinstance(node, nuke.BackdropNode): created_backdrops.append(node) bd_orders.add(node.knob("z_order").getValue()) if not bd_orders: return sib_orders = set() for node_name in placeholder.data["siblings"]: node = nuke.toNode(node_name) if isinstance(node, nuke.BackdropNode): sib_orders.add(node.knob("z_order").getValue()) if not sib_orders: return min_order = min(bd_orders) max_order = max(sib_orders) for backdrop_node in created_backdrops: z_order = backdrop_node.knob("z_order").getValue() backdrop_node.knob("z_order").setValue( z_order + max_order - min_order + 1) def _imprint_siblings(self, placeholder): """ - add siblings names to placeholder attributes (nodes created with it) - add Id to the attributes of all the other nodes """ created_nodes = placeholder.data["last_created"] created_nodes_set = set(created_nodes) for node in created_nodes: node_knobs = node.knobs() if ( "is_placeholder" not in node_knobs or ( "is_placeholder" in node_knobs and node.knob("is_placeholder").value() ) ): siblings = list(created_nodes_set - {node}) siblings_name = get_names_from_nodes(siblings) siblings = {"siblings": siblings_name} imprint(node, siblings) def _imprint_inits(self): """Add initial positions and dimensions to the attributes""" for node in nuke.allNodes(): refresh_node(node) imprint(node, {"x_init": node.xpos(), "y_init": node.ypos()}) node.knob("x_init").setVisible(False) node.knob("y_init").setVisible(False) width = node.screenWidth() height = node.screenHeight() if "bdwidth" in node.knobs(): imprint(node, {"w_init": width, "h_init": height}) node.knob("w_init").setVisible(False) node.knob("h_init").setVisible(False) refresh_node(node) def _update_nodes( self, placeholder, nodes, considered_nodes, offset_y=None ): """Adjust backdrop nodes dimensions and positions. Considering some nodes sizes. Args: nodes (list): list of nodes to update considered_nodes (list): list of nodes to consider while updating positions and dimensions offset (int): distance between copies """ placeholder_node = nuke.toNode(placeholder.scene_identifier) min_x, min_y, max_x, max_y = get_extreme_positions(considered_nodes) diff_x = diff_y = 0 contained_nodes = [] # for backdrops if offset_y is None: width_ph = placeholder_node.screenWidth() height_ph = placeholder_node.screenHeight() diff_y = max_y - min_y - height_ph diff_x = max_x - min_x - width_ph contained_nodes = [placeholder_node] min_x = placeholder_node.xpos() min_y = placeholder_node.ypos() else: siblings = get_nodes_by_names(placeholder.data["siblings"]) minX, _, maxX, _ = get_extreme_positions(siblings) diff_y = max_y - min_y + 20 diff_x = abs(max_x - min_x - maxX + minX) contained_nodes = considered_nodes if diff_y <= 0 and diff_x <= 0: return for node in nodes: refresh_node(node) if ( node == placeholder_node or node in considered_nodes ): continue if ( not isinstance(node, nuke.BackdropNode) or ( isinstance(node, nuke.BackdropNode) and not set(contained_nodes) <= set(node.getNodes()) ) ): if offset_y is None and node.xpos() >= min_x: node.setXpos(node.xpos() + diff_x) if node.ypos() >= min_y: node.setYpos(node.ypos() + diff_y) else: width = node.screenWidth() height = node.screenHeight() node.knob("bdwidth").setValue(width + diff_x) node.knob("bdheight").setValue(height + diff_y) refresh_node(node) def _set_created_connections(self, placeholder): """ set inputs and outputs of created nodes""" placeholder_node = nuke.toNode(placeholder.scene_identifier) input_node, output_node = get_group_io_nodes( placeholder.data["last_created"] ) for node in placeholder_node.dependent(): for idx in range(node.inputs()): if node.input(idx) == placeholder_node and output_node: node.setInput(idx, output_node) for node in placeholder_node.dependencies(): for idx in range(placeholder_node.inputs()): if placeholder_node.input(idx) == node and input_node: input_node.setInput(0, node) def _create_sib_copies(self, placeholder): """ creating copies of the palce_holder siblings (the ones who were created with it) for the new nodes added Returns : copies (dict) : with copied nodes names and their copies """ copies = {} siblings = get_nodes_by_names(placeholder.data["siblings"]) for node in siblings: new_node = duplicate_node(node) x_init = int(new_node.knob("x_init").getValue()) y_init = int(new_node.knob("y_init").getValue()) new_node.setXYpos(x_init, y_init) if isinstance(new_node, nuke.BackdropNode): w_init = new_node.knob("w_init").getValue() h_init = new_node.knob("h_init").getValue() new_node.knob("bdwidth").setValue(w_init) new_node.knob("bdheight").setValue(h_init) refresh_node(node) if "repre_id" in node.knobs().keys(): node.removeKnob(node.knob("repre_id")) copies[node.name()] = new_node return copies def _set_copies_connections(self, placeholder, copies): """Set inputs and outputs of the copies. Args: copies (dict): Copied nodes by their names. """ last_input, last_output = get_group_io_nodes( placeholder.data["last_created"] ) siblings = get_nodes_by_names(placeholder.data["siblings"]) siblings_input, siblings_output = get_group_io_nodes(siblings) copy_input = copies[siblings_input.name()] copy_output = copies[siblings_output.name()] for node_init in siblings: if node_init == siblings_output: continue node_copy = copies[node_init.name()] for node in node_init.dependent(): for idx in range(node.inputs()): if node.input(idx) != node_init: continue if node in siblings: copies[node.name()].setInput(idx, node_copy) else: last_input.setInput(0, node_copy) for node in node_init.dependencies(): for idx in range(node_init.inputs()): if node_init.input(idx) != node: continue if node_init == siblings_input: copy_input.setInput(idx, node) elif node in siblings: node_copy.setInput(idx, copies[node.name()]) else: node_copy.setInput(idx, last_output) siblings_input.setInput(0, copy_output) def build_workfile_template(*args, **kwargs): builder = NukeTemplateBuilder(registered_host()) builder.build_template(*args, **kwargs) # set all settings to shot context default WorkfileSettings().set_context_settings() def update_workfile_template(*args): builder = NukeTemplateBuilder(registered_host()) builder.rebuild_template() def create_placeholder(*args): host = registered_host() builder = NukeTemplateBuilder(host) window = WorkfileBuildPlaceholderDialog(host, builder, parent=get_main_window()) window.show() def update_placeholder(*args): host = registered_host() builder = NukeTemplateBuilder(host) placeholder_items_by_id = { placeholder_item.scene_identifier: placeholder_item for placeholder_item in builder.get_placeholders() } placeholder_items = [] for node in nuke.selectedNodes(): node_name = node.fullName() if node_name in placeholder_items_by_id: placeholder_items.append(placeholder_items_by_id[node_name]) # TODO show UI at least if len(placeholder_items) == 0: raise ValueError("No node selected") if len(placeholder_items) > 1: raise ValueError("Too many selected nodes") placeholder_item = placeholder_items[0] window = WorkfileBuildPlaceholderDialog(host, builder, parent=get_main_window()) window.set_update_mode(placeholder_item) window.exec_() ================================================ FILE: openpype/hosts/nuke/api/workio.py ================================================ """Host API required Work Files tool""" import os import nuke import shutil from .utils import is_headless def file_extensions(): return [".nk"] def has_unsaved_changes(): return nuke.root().modified() def save_file(filepath): path = filepath.replace("\\", "/") nuke.scriptSaveAs(path, overwrite=1) nuke.Root()["name"].setValue(path) nuke.Root()["project_directory"].setValue(os.path.dirname(path)) nuke.Root().setModified(False) def open_file(filepath): def read_script(nuke_script): nuke.scriptClear() nuke.scriptReadFile(nuke_script) nuke.Root()["name"].setValue(nuke_script) nuke.Root()["project_directory"].setValue(os.path.dirname(nuke_script)) nuke.Root().setModified(False) filepath = filepath.replace("\\", "/") # To remain in the same window, we have to clear the script and read # in the contents of the workfile. # Nuke Preferences can be read after the script is read. read_script(filepath) if not is_headless(): autosave = nuke.toNode("preferences")["AutoSaveName"].evaluate() autosave_prmpt = "Autosave detected.\n" \ "Would you like to load the autosave file?" # noqa if os.path.isfile(autosave) and nuke.ask(autosave_prmpt): try: # Overwrite the filepath with autosave shutil.copy(autosave, filepath) # Now read the (auto-saved) script again read_script(filepath) except shutil.Error as err: nuke.message( "Detected autosave file could not be used.\n{}" .format(err)) return True def current_file(): current_file = nuke.root().name() # Unsaved current file if current_file == 'Root': return None return os.path.normpath(current_file).replace("\\", "/") def work_root(session): work_dir = session["AVALON_WORKDIR"] scene_dir = session.get("AVALON_SCENEDIR") if scene_dir: path = os.path.join(work_dir, scene_dir) else: path = work_dir return os.path.normpath(path).replace("\\", "/") ================================================ FILE: openpype/hosts/nuke/hooks/pre_nukeassist_setup.py ================================================ from openpype.lib.applications import PreLaunchHook class PrelaunchNukeAssistHook(PreLaunchHook): """ Adding flag when nukeassist """ app_groups = {"nukeassist"} launch_types = set() def execute(self): self.launch_context.env["NUKEASSIST"] = "1" ================================================ FILE: openpype/hosts/nuke/plugins/__init__.py ================================================ ================================================ FILE: openpype/hosts/nuke/plugins/create/__init__.py ================================================ ================================================ FILE: openpype/hosts/nuke/plugins/create/convert_legacy.py ================================================ from openpype.pipeline.create.creator_plugins import SubsetConvertorPlugin from openpype.hosts.nuke.api.lib import ( INSTANCE_DATA_KNOB, get_node_data, get_avalon_knob_data, AVALON_TAB, ) from openpype.hosts.nuke.api.plugin import convert_to_valid_instaces import nuke class LegacyConverted(SubsetConvertorPlugin): identifier = "legacy.converter" def find_instances(self): legacy_found = False # search for first available legacy item for node in nuke.allNodes(recurseGroups=True): if node.Class() in ["Viewer", "Dot"]: continue if get_node_data(node, INSTANCE_DATA_KNOB): continue if AVALON_TAB not in node.knobs(): continue # get data from avalon knob avalon_knob_data = get_avalon_knob_data( node, ["avalon:", "ak:"], create=False) if not avalon_knob_data: continue if avalon_knob_data["id"] != "pyblish.avalon.instance": continue # catch and break legacy_found = True break if legacy_found: # if not item do not add legacy instance converter self.add_convertor_item("Convert legacy instances") def convert(self): # loop all instances and convert them convert_to_valid_instaces() # remove legacy item if all is fine self.remove_convertor_item() ================================================ FILE: openpype/hosts/nuke/plugins/create/create_backdrop.py ================================================ from nukescripts import autoBackdrop from openpype.hosts.nuke.api import ( NukeCreator, maintained_selection, select_nodes ) class CreateBackdrop(NukeCreator): """Add Publishable Backdrop""" identifier = "create_backdrop" label = "Nukenodes (backdrop)" family = "nukenodes" icon = "file-archive-o" maintain_selection = True # plugin attributes node_color = "0xdfea5dff" def create_instance_node( self, node_name, knobs=None, parent=None, node_type=None ): with maintained_selection(): if len(self.selected_nodes) >= 1: select_nodes(self.selected_nodes) created_node = autoBackdrop() created_node["name"].setValue(node_name) created_node["tile_color"].setValue(int(self.node_color, 16)) created_node["note_font_size"].setValue(24) created_node["label"].setValue("[{}]".format(node_name)) return created_node def create(self, subset_name, instance_data, pre_create_data): # make sure subset name is unique self.check_existing_subset(subset_name) instance = super(CreateBackdrop, self).create( subset_name, instance_data, pre_create_data ) return instance ================================================ FILE: openpype/hosts/nuke/plugins/create/create_camera.py ================================================ import nuke from openpype.hosts.nuke.api import ( NukeCreator, NukeCreatorError, maintained_selection ) from openpype.hosts.nuke.api.lib import ( create_camera_node_by_version ) class CreateCamera(NukeCreator): """Add Publishable Camera""" identifier = "create_camera" label = "Camera (3d)" family = "camera" icon = "camera" # plugin attributes node_color = "0xff9100ff" def create_instance_node( self, node_name, knobs=None, parent=None, node_type=None ): with maintained_selection(): if self.selected_nodes: node = self.selected_nodes[0] if node.Class() != "Camera3": raise NukeCreatorError( "Creator error: Select only camera node type") created_node = self.selected_nodes[0] else: created_node = create_camera_node_by_version() created_node["tile_color"].setValue( int(self.node_color, 16)) created_node["name"].setValue(node_name) return created_node def create(self, subset_name, instance_data, pre_create_data): # make sure subset name is unique self.check_existing_subset(subset_name) instance = super(CreateCamera, self).create( subset_name, instance_data, pre_create_data ) return instance def set_selected_nodes(self, pre_create_data): if pre_create_data.get("use_selection"): self.selected_nodes = nuke.selectedNodes() if self.selected_nodes == []: raise NukeCreatorError( "Creator error: No active selection") elif len(self.selected_nodes) > 1: raise NukeCreatorError( "Creator error: Select only one camera node") else: self.selected_nodes = [] ================================================ FILE: openpype/hosts/nuke/plugins/create/create_gizmo.py ================================================ import nuke from openpype.hosts.nuke.api import ( NukeCreator, NukeCreatorError, maintained_selection ) class CreateGizmo(NukeCreator): """Add Publishable Group as gizmo""" identifier = "create_gizmo" label = "Gizmo (group)" family = "gizmo" icon = "file-archive-o" default_variants = ["ViewerInput", "Lut", "Effect"] # plugin attributes node_color = "0x7533c1ff" def create_instance_node( self, node_name, knobs=None, parent=None, node_type=None ): with maintained_selection(): if self.selected_nodes: node = self.selected_nodes[0] if node.Class() != "Group": raise NukeCreatorError( "Creator error: Select only 'Group' node type") created_node = node else: created_node = nuke.collapseToGroup() created_node["tile_color"].setValue( int(self.node_color, 16)) created_node["name"].setValue(node_name) return created_node def create(self, subset_name, instance_data, pre_create_data): # make sure subset name is unique self.check_existing_subset(subset_name) instance = super(CreateGizmo, self).create( subset_name, instance_data, pre_create_data ) return instance def set_selected_nodes(self, pre_create_data): if pre_create_data.get("use_selection"): self.selected_nodes = nuke.selectedNodes() if self.selected_nodes == []: raise NukeCreatorError("Creator error: No active selection") elif len(self.selected_nodes) > 1: NukeCreatorError("Creator error: Select only one 'Group' node") else: self.selected_nodes = [] ================================================ FILE: openpype/hosts/nuke/plugins/create/create_model.py ================================================ import nuke from openpype.hosts.nuke.api import ( NukeCreator, NukeCreatorError, maintained_selection ) class CreateModel(NukeCreator): """Add Publishable Camera""" identifier = "create_model" label = "Model (3d)" family = "model" icon = "cube" default_variants = ["Main"] # plugin attributes node_color = "0xff3200ff" def create_instance_node( self, node_name, knobs=None, parent=None, node_type=None ): with maintained_selection(): if self.selected_nodes: node = self.selected_nodes[0] if node.Class() != "Scene": raise NukeCreatorError( "Creator error: Select only 'Scene' node type") created_node = node else: created_node = nuke.createNode("Scene") created_node["tile_color"].setValue( int(self.node_color, 16)) created_node["name"].setValue(node_name) return created_node def create(self, subset_name, instance_data, pre_create_data): # make sure subset name is unique self.check_existing_subset(subset_name) instance = super(CreateModel, self).create( subset_name, instance_data, pre_create_data ) return instance def set_selected_nodes(self, pre_create_data): if pre_create_data.get("use_selection"): self.selected_nodes = nuke.selectedNodes() if self.selected_nodes == []: raise NukeCreatorError("Creator error: No active selection") elif len(self.selected_nodes) > 1: NukeCreatorError("Creator error: Select only one 'Scene' node") else: self.selected_nodes = [] ================================================ FILE: openpype/hosts/nuke/plugins/create/create_source.py ================================================ import nuke import six import sys from openpype.hosts.nuke.api import ( INSTANCE_DATA_KNOB, NukeCreator, NukeCreatorError, set_node_data ) from openpype.pipeline import ( CreatedInstance ) class CreateSource(NukeCreator): """Add Publishable Read with source""" identifier = "create_source" label = "Source (read)" family = "source" icon = "film" default_variants = ["Effect", "Backplate", "Fire", "Smoke"] # plugin attributes node_color = "0xff9100ff" def create_instance_node( self, node_name, read_node ): read_node["tile_color"].setValue( int(self.node_color, 16)) read_node["name"].setValue(node_name) return read_node def create(self, subset_name, instance_data, pre_create_data): # make sure selected nodes are added self.set_selected_nodes(pre_create_data) try: for read_node in self.selected_nodes: if read_node.Class() != 'Read': continue node_name = read_node.name() _subset_name = subset_name + node_name # make sure subset name is unique self.check_existing_subset(_subset_name) instance_node = self.create_instance_node( _subset_name, read_node ) instance = CreatedInstance( self.family, _subset_name, instance_data, self ) instance.transient_data["node"] = instance_node self._add_instance_to_context(instance) set_node_data( instance_node, INSTANCE_DATA_KNOB, instance.data_to_store() ) except Exception as er: six.reraise( NukeCreatorError, NukeCreatorError("Creator error: {}".format(er)), sys.exc_info()[2]) def set_selected_nodes(self, pre_create_data): if pre_create_data.get("use_selection"): self.selected_nodes = nuke.selectedNodes() if self.selected_nodes == []: raise NukeCreatorError("Creator error: No active selection") else: NukeCreatorError( "Creator error: only supported with active selection") ================================================ FILE: openpype/hosts/nuke/plugins/create/create_write_image.py ================================================ import nuke import sys import six from openpype.pipeline import ( CreatedInstance ) from openpype.lib import ( BoolDef, NumberDef, UISeparatorDef, EnumDef ) from openpype.hosts.nuke import api as napi from openpype.hosts.nuke.api.plugin import exposed_write_knobs class CreateWriteImage(napi.NukeWriteCreator): identifier = "create_write_image" label = "Image (write)" family = "image" icon = "sign-out" instance_attributes = [ "use_range_limit" ] default_variants = [ "StillFrame", "MPFrame", "LayoutFrame" ] temp_rendering_path_template = ( "{work}/renders/nuke/{subset}/{subset}.{frame}.{ext}") def get_pre_create_attr_defs(self): attr_defs = [ BoolDef( "use_selection", default=not self.create_context.headless, label="Use selection" ), self._get_render_target_enum(), UISeparatorDef(), self._get_frame_source_number() ] return attr_defs def _get_render_target_enum(self): rendering_targets = { "local": "Local machine rendering", "frames": "Use existing frames" } return EnumDef( "render_target", items=rendering_targets, label="Render target" ) def _get_frame_source_number(self): return NumberDef( "active_frame", label="Active frame", default=nuke.frame() ) def create_instance_node(self, subset_name, instance_data): # add fpath_template write_data = { "creator": self.__class__.__name__, "subset": subset_name, "fpath_template": self.temp_rendering_path_template } write_data.update(instance_data) created_node = napi.create_write_node( subset_name, write_data, input=self.selected_node, prenodes=self.prenodes, linked_knobs=self.get_linked_knobs(), **{ "frame": nuke.frame() } ) self._add_frame_range_limit(created_node, instance_data) self.integrate_links(created_node, outputs=True) return created_node def create(self, subset_name, instance_data, pre_create_data): subset_name = subset_name.format(**pre_create_data) # pass values from precreate to instance self.pass_pre_attributes_to_instance( instance_data, pre_create_data, [ "active_frame", "render_target" ] ) # make sure selected nodes are added self.set_selected_nodes(pre_create_data) # make sure subset name is unique self.check_existing_subset(subset_name) instance_node = self.create_instance_node( subset_name, instance_data, ) try: instance = CreatedInstance( self.family, subset_name, instance_data, self ) instance.transient_data["node"] = instance_node self._add_instance_to_context(instance) napi.set_node_data( instance_node, napi.INSTANCE_DATA_KNOB, instance.data_to_store() ) exposed_write_knobs( self.project_settings, self.__class__.__name__, instance_node ) return instance except Exception as er: six.reraise( napi.NukeCreatorError, napi.NukeCreatorError("Creator error: {}".format(er)), sys.exc_info()[2] ) def _add_frame_range_limit(self, write_node, instance_data): if "use_range_limit" not in self.instance_attributes: return active_frame = ( instance_data["creator_attributes"].get("active_frame")) write_node.begin() for n in nuke.allNodes(): # get write node if n.Class() in "Write": w_node = n write_node.end() w_node["use_limit"].setValue(True) w_node["first"].setValue(active_frame or nuke.frame()) w_node["last"].setExpression("first") return write_node ================================================ FILE: openpype/hosts/nuke/plugins/create/create_write_prerender.py ================================================ import nuke import sys import six from openpype.pipeline import ( CreatedInstance ) from openpype.lib import ( BoolDef ) from openpype.hosts.nuke import api as napi from openpype.hosts.nuke.api.plugin import exposed_write_knobs class CreateWritePrerender(napi.NukeWriteCreator): identifier = "create_write_prerender" label = "Prerender (write)" family = "prerender" icon = "sign-out" instance_attributes = [ "use_range_limit" ] default_variants = [ "Key01", "Bg01", "Fg01", "Branch01", "Part01" ] temp_rendering_path_template = ( "{work}/renders/nuke/{subset}/{subset}.{frame}.{ext}") # Before write node render. order = 90 def get_pre_create_attr_defs(self): attr_defs = [ BoolDef( "use_selection", default=not self.create_context.headless, label="Use selection" ), self._get_render_target_enum() ] return attr_defs def create_instance_node(self, subset_name, instance_data): # add fpath_template write_data = { "creator": self.__class__.__name__, "subset": subset_name, "fpath_template": self.temp_rendering_path_template } write_data.update(instance_data) # get width and height if self.selected_node: width, height = ( self.selected_node.width(), self.selected_node.height()) else: actual_format = nuke.root().knob('format').value() width, height = (actual_format.width(), actual_format.height()) created_node = napi.create_write_node( subset_name, write_data, input=self.selected_node, prenodes=self.prenodes, linked_knobs=self.get_linked_knobs(), **{ "width": width, "height": height } ) self._add_frame_range_limit(created_node) self.integrate_links(created_node, outputs=True) return created_node def create(self, subset_name, instance_data, pre_create_data): # pass values from precreate to instance self.pass_pre_attributes_to_instance( instance_data, pre_create_data, [ "render_target" ] ) # make sure selected nodes are added self.set_selected_nodes(pre_create_data) # make sure subset name is unique self.check_existing_subset(subset_name) instance_node = self.create_instance_node( subset_name, instance_data ) try: instance = CreatedInstance( self.family, subset_name, instance_data, self ) instance.transient_data["node"] = instance_node self._add_instance_to_context(instance) napi.set_node_data( instance_node, napi.INSTANCE_DATA_KNOB, instance.data_to_store() ) exposed_write_knobs( self.project_settings, self.__class__.__name__, instance_node ) return instance except Exception as er: six.reraise( napi.NukeCreatorError, napi.NukeCreatorError("Creator error: {}".format(er)), sys.exc_info()[2] ) def _add_frame_range_limit(self, write_node): if "use_range_limit" not in self.instance_attributes: return write_node.begin() for n in nuke.allNodes(): # get write node if n.Class() in "Write": w_node = n write_node.end() w_node["use_limit"].setValue(True) w_node["first"].setValue(nuke.root()["first_frame"].value()) w_node["last"].setValue(nuke.root()["last_frame"].value()) return write_node ================================================ FILE: openpype/hosts/nuke/plugins/create/create_write_render.py ================================================ import nuke import sys import six from openpype.pipeline import ( CreatedInstance ) from openpype.lib import ( BoolDef ) from openpype.hosts.nuke import api as napi from openpype.hosts.nuke.api.plugin import exposed_write_knobs class CreateWriteRender(napi.NukeWriteCreator): identifier = "create_write_render" label = "Render (write)" family = "render" icon = "sign-out" instance_attributes = [ "reviewable" ] default_variants = [ "Main", "Mask" ] temp_rendering_path_template = ( "{work}/renders/nuke/{subset}/{subset}.{frame}.{ext}") def get_pre_create_attr_defs(self): attr_defs = [ BoolDef( "use_selection", default=not self.create_context.headless, label="Use selection" ), self._get_render_target_enum() ] return attr_defs def create_instance_node(self, subset_name, instance_data): # add fpath_template write_data = { "creator": self.__class__.__name__, "subset": subset_name, "fpath_template": self.temp_rendering_path_template } write_data.update(instance_data) # get width and height if self.selected_node: width, height = ( self.selected_node.width(), self.selected_node.height()) else: actual_format = nuke.root().knob('format').value() width, height = (actual_format.width(), actual_format.height()) self.log.debug(">>>>>>> : {}".format(self.instance_attributes)) self.log.debug(">>>>>>> : {}".format(self.get_linked_knobs())) created_node = napi.create_write_node( subset_name, write_data, input=self.selected_node, prenodes=self.prenodes, linked_knobs=self.get_linked_knobs(), **{ "width": width, "height": height } ) self.integrate_links(created_node, outputs=False) return created_node def create(self, subset_name, instance_data, pre_create_data): # pass values from precreate to instance self.pass_pre_attributes_to_instance( instance_data, pre_create_data, [ "render_target" ] ) # make sure selected nodes are added self.set_selected_nodes(pre_create_data) # make sure subset name is unique self.check_existing_subset(subset_name) instance_node = self.create_instance_node( subset_name, instance_data ) try: instance = CreatedInstance( self.family, subset_name, instance_data, self ) instance.transient_data["node"] = instance_node self._add_instance_to_context(instance) napi.set_node_data( instance_node, napi.INSTANCE_DATA_KNOB, instance.data_to_store() ) exposed_write_knobs( self.project_settings, self.__class__.__name__, instance_node ) return instance except Exception as er: six.reraise( napi.NukeCreatorError, napi.NukeCreatorError("Creator error: {}".format(er)), sys.exc_info()[2] ) ================================================ FILE: openpype/hosts/nuke/plugins/create/workfile_creator.py ================================================ import openpype.hosts.nuke.api as api from openpype.client import get_asset_by_name from openpype.pipeline import ( AutoCreator, CreatedInstance, ) from openpype.hosts.nuke.api import ( INSTANCE_DATA_KNOB, set_node_data ) import nuke class WorkfileCreator(AutoCreator): identifier = "workfile" family = "workfile" default_variant = "Main" def get_instance_attr_defs(self): return [] def collect_instances(self): root_node = nuke.root() instance_data = api.get_node_data( root_node, api.INSTANCE_DATA_KNOB ) project_name = self.create_context.get_current_project_name() asset_name = self.create_context.get_current_asset_name() task_name = self.create_context.get_current_task_name() host_name = self.create_context.host_name asset_doc = get_asset_by_name(project_name, asset_name) subset_name = self.get_subset_name( self.default_variant, task_name, asset_doc, project_name, host_name ) instance_data.update({ "asset": asset_name, "task": task_name, "variant": self.default_variant }) instance_data.update(self.get_dynamic_data( self.default_variant, task_name, asset_doc, project_name, host_name, instance_data )) instance = CreatedInstance( self.family, subset_name, instance_data, self ) instance.transient_data["node"] = root_node self._add_instance_to_context(instance) def update_instances(self, update_list): for created_inst, _changes in update_list: instance_node = created_inst.transient_data["node"] set_node_data( instance_node, INSTANCE_DATA_KNOB, created_inst.data_to_store() ) def create(self, options=None): # no need to create if it is created # in `collect_instances` pass ================================================ FILE: openpype/hosts/nuke/plugins/inventory/repair_old_loaders.py ================================================ from openpype.lib import Logger from openpype.pipeline import InventoryAction from openpype.hosts.nuke.api.lib import set_avalon_knob_data class RepairOldLoaders(InventoryAction): label = "Repair Old Loaders" icon = "gears" color = "#cc0000" log = Logger.get_logger(__name__) def process(self, containers): import nuke new_loader = "LoadClip" for cdata in containers: orig_loader = cdata["loader"] orig_name = cdata["objectName"] if orig_loader not in ["LoadSequence", "LoadMov"]: self.log.warning( "This repair action is only working on " "`LoadSequence` and `LoadMov` Loaders") continue new_name = orig_name.replace(orig_loader, new_loader) node = nuke.toNode(cdata["objectName"]) cdata.update({ "loader": new_loader, "objectName": new_name }) node["name"].setValue(new_name) # get data from avalon knob set_avalon_knob_data(node, cdata) ================================================ FILE: openpype/hosts/nuke/plugins/inventory/select_containers.py ================================================ from openpype.pipeline import InventoryAction from openpype.hosts.nuke.api.command import viewer_update_and_undo_stop class SelectContainers(InventoryAction): label = "Select Containers" icon = "mouse-pointer" color = "#d8d8d8" def process(self, containers): import nuke nodes = [nuke.toNode(i["objectName"]) for i in containers] with viewer_update_and_undo_stop(): # clear previous_selection [n['selected'].setValue(False) for n in nodes] # Select tool for node in nodes: node["selected"].setValue(True) ================================================ FILE: openpype/hosts/nuke/plugins/load/actions.py ================================================ """A module containing generic loader actions that will display in the Loader. """ from openpype.lib import Logger from openpype.pipeline import load log = Logger.get_logger(__name__) class SetFrameRangeLoader(load.LoaderPlugin): """Set frame range excluding pre- and post-handles""" families = ["animation", "camera", "write", "yeticache", "pointcache"] representations = ["*"] extensions = {"*"} label = "Set frame range" order = 11 icon = "clock-o" color = "white" def load(self, context, name, namespace, data): from openpype.hosts.nuke.api import lib version = context['version'] version_data = version.get("data", {}) start = version_data.get("frameStart", None) end = version_data.get("frameEnd", None) log.info("start: {}, end: {}".format(start, end)) if start is None or end is None: log.info("Skipping setting frame range because start or " "end frame data is missing..") return lib.update_frame_range(start, end) class SetFrameRangeWithHandlesLoader(load.LoaderPlugin): """Set frame range including pre- and post-handles""" families = ["animation", "camera", "write", "yeticache", "pointcache"] representations = ["*"] label = "Set frame range (with handles)" order = 12 icon = "clock-o" color = "white" def load(self, context, name, namespace, data): from openpype.hosts.nuke.api import lib version = context['version'] version_data = version.get("data", {}) start = version_data.get("frameStart", None) end = version_data.get("frameEnd", None) if start is None or end is None: print("Skipping setting frame range because start or " "end frame data is missing..") return # Include handles start -= version_data.get("handleStart", 0) end += version_data.get("handleEnd", 0) lib.update_frame_range(start, end) ================================================ FILE: openpype/hosts/nuke/plugins/load/load_backdrop.py ================================================ import nuke import nukescripts from openpype.client import ( get_version_by_id, get_last_version_by_subset_id, ) from openpype.pipeline import ( load, get_current_project_name, get_representation_path, ) from openpype.hosts.nuke.api.lib import ( find_free_space_to_paste_nodes, maintained_selection, reset_selection, select_nodes, get_avalon_knob_data, set_avalon_knob_data ) from openpype.hosts.nuke.api.command import viewer_update_and_undo_stop from openpype.hosts.nuke.api import containerise, update_container class LoadBackdropNodes(load.LoaderPlugin): """Loading Published Backdrop nodes (workfile, nukenodes)""" families = ["workfile", "nukenodes", "matchmove"] representations = ["*"] extensions = {"nk"} label = "Import Nuke Nodes" order = 0 icon = "eye" color = "white" node_color = "0x7533c1ff" def load(self, context, name, namespace, data): """ Loading function to import .nk file into script and wrap it on backdrop Arguments: context (dict): context of version name (str): name of the version namespace (str): asset name data (dict): compulsory attribute > not used Returns: nuke node: containerised nuke node object """ # get main variables version = context['version'] version_data = version.get("data", {}) vname = version.get("name", None) namespace = namespace or context['asset']['name'] colorspace = version_data.get("colorspace", None) object_name = "{}_{}".format(name, namespace) # prepare data for imprinting # add additional metadata from the version to imprint to Avalon knob add_keys = ["source", "author", "fps"] data_imprint = { "version": vname, "colorspaceInput": colorspace } for k in add_keys: data_imprint.update({k: version_data[k]}) # getting file path file = self.filepath_from_context(context).replace("\\", "/") # adding nodes to node graph # just in case we are in group lets jump out of it nuke.endGroup() # Get mouse position n = nuke.createNode("NoOp") xcursor, ycursor = (n.xpos(), n.ypos()) reset_selection() nuke.delete(n) bdn_frame = 50 with maintained_selection(): # add group from nk nuke.nodePaste(file) # get all pasted nodes new_nodes = list() nodes = nuke.selectedNodes() # get pointer position in DAG xpointer, ypointer = find_free_space_to_paste_nodes( nodes, direction="right", offset=200 + bdn_frame ) # reset position to all nodes and replace inputs and output for n in nodes: reset_selection() xpos = (n.xpos() - xcursor) + xpointer ypos = (n.ypos() - ycursor) + ypointer n.setXYpos(xpos, ypos) # replace Input nodes for dots if n.Class() in "Input": dot = nuke.createNode("Dot") new_name = n.name().replace("INP", "DOT") dot.setName(new_name) dot["label"].setValue(new_name) dot.setXYpos(xpos, ypos) new_nodes.append(dot) # rewire dep = n.dependent() for d in dep: index = next((i for i, dpcy in enumerate( d.dependencies()) if n is dpcy), 0) d.setInput(index, dot) # remove Input node reset_selection() nuke.delete(n) continue # replace Input nodes for dots elif n.Class() in "Output": dot = nuke.createNode("Dot") new_name = n.name() + "_DOT" dot.setName(new_name) dot["label"].setValue(new_name) dot.setXYpos(xpos, ypos) new_nodes.append(dot) # rewire dep = next((d for d in n.dependencies()), None) if dep: dot.setInput(0, dep) # remove Input node reset_selection() nuke.delete(n) continue else: new_nodes.append(n) # reselect nodes with new Dot instead of Inputs and Output reset_selection() select_nodes(new_nodes) # place on backdrop bdn = nukescripts.autoBackdrop() # add frame offset xpos = bdn.xpos() - bdn_frame ypos = bdn.ypos() - bdn_frame bdwidth = bdn["bdwidth"].value() + (bdn_frame*2) bdheight = bdn["bdheight"].value() + (bdn_frame*2) bdn["xpos"].setValue(xpos) bdn["ypos"].setValue(ypos) bdn["bdwidth"].setValue(bdwidth) bdn["bdheight"].setValue(bdheight) bdn["name"].setValue(object_name) bdn["label"].setValue("Version tracked frame: \n`{}`\n\nPLEASE DO NOT REMOVE OR MOVE \nANYTHING FROM THIS FRAME!".format(object_name)) bdn["note_font_size"].setValue(20) return containerise( node=bdn, name=name, namespace=namespace, context=context, loader=self.__class__.__name__, data=data_imprint) def update(self, container, representation): """Update the Loader's path Nuke automatically tries to reset some variables when changing the loader's path to a new file. These automatic changes are to its inputs: """ # get main variables # Get version from io project_name = get_current_project_name() version_doc = get_version_by_id(project_name, representation["parent"]) # get corresponding node GN = container["node"] file = get_representation_path(representation).replace("\\", "/") name = container['name'] version_data = version_doc.get("data", {}) vname = version_doc.get("name", None) namespace = container['namespace'] colorspace = version_data.get("colorspace", None) object_name = "{}_{}".format(name, namespace) add_keys = ["source", "author", "fps"] data_imprint = { "representation": str(representation["_id"]), "version": vname, "colorspaceInput": colorspace, } for k in add_keys: data_imprint.update({k: version_data[k]}) # adding nodes to node graph # just in case we are in group lets jump out of it nuke.endGroup() with maintained_selection(): xpos = GN.xpos() ypos = GN.ypos() avalon_data = get_avalon_knob_data(GN) nuke.delete(GN) # add group from nk nuke.nodePaste(file) GN = nuke.selectedNode() set_avalon_knob_data(GN, avalon_data) GN.setXYpos(xpos, ypos) GN["name"].setValue(object_name) # get all versions in list last_version_doc = get_last_version_by_subset_id( project_name, version_doc["parent"], fields=["_id"] ) # change color of node if version_doc["_id"] == last_version_doc["_id"]: color_value = self.node_color else: color_value = "0xd88467ff" GN["tile_color"].setValue(int(color_value, 16)) self.log.info("updated to version: {}".format(version_doc.get("name"))) return update_container(GN, data_imprint) def switch(self, container, representation): self.update(container, representation) def remove(self, container): node = container["node"] with viewer_update_and_undo_stop(): nuke.delete(node) ================================================ FILE: openpype/hosts/nuke/plugins/load/load_camera_abc.py ================================================ import nuke from openpype.client import ( get_version_by_id, get_last_version_by_subset_id ) from openpype.pipeline import ( load, get_current_project_name, get_representation_path, ) from openpype.hosts.nuke.api import ( containerise, update_container, viewer_update_and_undo_stop ) from openpype.hosts.nuke.api.lib import ( maintained_selection ) class AlembicCameraLoader(load.LoaderPlugin): """ This will load alembic camera into script. """ families = ["camera"] representations = ["*"] extensions = {"abc"} label = "Load Alembic Camera" icon = "camera" color = "orange" node_color = "0x3469ffff" def load(self, context, name, namespace, data): # get main variables version = context['version'] version_data = version.get("data", {}) vname = version.get("name", None) first = version_data.get("frameStart", None) last = version_data.get("frameEnd", None) fps = version_data.get("fps") or nuke.root()["fps"].getValue() namespace = namespace or context['asset']['name'] object_name = "{}_{}".format(name, namespace) # prepare data for imprinting # add additional metadata from the version to imprint to Avalon knob add_keys = ["source", "author", "fps"] data_imprint = { "frameStart": first, "frameEnd": last, "version": vname, } for k in add_keys: data_imprint.update({k: version_data[k]}) # getting file path file = self.filepath_from_context(context).replace("\\", "/") with maintained_selection(): camera_node = nuke.createNode( "Camera2", "name {} file {} read_from_file True".format( object_name, file), inpanel=False ) camera_node.forceValidate() camera_node["frame_rate"].setValue(float(fps)) # workaround because nuke's bug is not adding # animation keys properly xpos = camera_node.xpos() ypos = camera_node.ypos() nuke.nodeCopy("%clipboard%") nuke.delete(camera_node) nuke.nodePaste("%clipboard%") camera_node = nuke.toNode(object_name) camera_node.setXYpos(xpos, ypos) # color node by correct color by actual version self.node_version_color(version, camera_node) return containerise( node=camera_node, name=name, namespace=namespace, context=context, loader=self.__class__.__name__, data=data_imprint) def update(self, container, representation): """ Called by Scene Inventory when look should be updated to current version. If any reference edits cannot be applied, eg. shader renamed and material not present, reference is unloaded and cleaned. All failed edits are highlighted to the user via message box. Args: container: object that has look to be updated representation: (dict): relationship data to get proper representation from DB and persisted data in .json Returns: None """ # Get version from io project_name = get_current_project_name() version_doc = get_version_by_id(project_name, representation["parent"]) # get main variables version_data = version_doc.get("data", {}) vname = version_doc.get("name", None) first = version_data.get("frameStart", None) last = version_data.get("frameEnd", None) fps = version_data.get("fps") or nuke.root()["fps"].getValue() # prepare data for imprinting # add additional metadata from the version to imprint to Avalon knob add_keys = ["source", "author", "fps"] data_imprint = { "representation": str(representation["_id"]), "frameStart": first, "frameEnd": last, "version": vname } for k in add_keys: data_imprint.update({k: version_data[k]}) # getting file path file = get_representation_path(representation).replace("\\", "/") with maintained_selection(): camera_node = container["node"] camera_node['selected'].setValue(True) # collect input output dependencies dependencies = camera_node.dependencies() dependent = camera_node.dependent() camera_node["frame_rate"].setValue(float(fps)) camera_node["file"].setValue(file) # workaround because nuke's bug is # not adding animation keys properly xpos = camera_node.xpos() ypos = camera_node.ypos() nuke.nodeCopy("%clipboard%") camera_name = camera_node.name() nuke.delete(camera_node) nuke.nodePaste("%clipboard%") camera_node = nuke.toNode(camera_name) camera_node.setXYpos(xpos, ypos) # link to original input nodes for i, input in enumerate(dependencies): camera_node.setInput(i, input) # link to original output nodes for d in dependent: index = next((i for i, dpcy in enumerate( d.dependencies()) if camera_node is dpcy), 0) d.setInput(index, camera_node) # color node by correct color by actual version self.node_version_color(version_doc, camera_node) self.log.info("updated to version: {}".format(version_doc.get("name"))) return update_container(camera_node, data_imprint) def node_version_color(self, version_doc, node): """ Coloring a node by correct color by actual version """ # get all versions in list project_name = get_current_project_name() last_version_doc = get_last_version_by_subset_id( project_name, version_doc["parent"], fields=["_id"] ) # change color of node if version_doc["_id"] == last_version_doc["_id"]: color_value = self.node_color else: color_value = "0xd88467ff" node["tile_color"].setValue(int(color_value, 16)) def switch(self, container, representation): self.update(container, representation) def remove(self, container): node = container["node"] with viewer_update_and_undo_stop(): nuke.delete(node) ================================================ FILE: openpype/hosts/nuke/plugins/load/load_clip.py ================================================ import nuke import qargparse from pprint import pformat from copy import deepcopy from openpype.lib import Logger from openpype.client import ( get_version_by_id, get_last_version_by_subset_id, ) from openpype.pipeline import ( get_current_project_name, get_representation_path, ) from openpype.pipeline.colorspace import ( get_imageio_file_rules_colorspace_from_filepath ) from openpype.hosts.nuke.api.lib import ( get_imageio_input_colorspace, maintained_selection ) from openpype.hosts.nuke.api import ( containerise, update_container, viewer_update_and_undo_stop, colorspace_exists_on_node ) from openpype.lib.transcoding import ( VIDEO_EXTENSIONS, IMAGE_EXTENSIONS ) from openpype.hosts.nuke.api import plugin class LoadClip(plugin.NukeLoader): """Load clip into Nuke Either it is image sequence or video file. """ log = Logger.get_logger(__name__) families = [ "source", "plate", "render", "prerender", "review" ] representations = ["*"] extensions = set( ext.lstrip(".") for ext in IMAGE_EXTENSIONS.union(VIDEO_EXTENSIONS) ) label = "Load Clip" order = -20 icon = "file-video-o" color = "white" # Loaded from settings _representations = [] script_start = int(nuke.root()["first_frame"].value()) # option gui options_defaults = { "start_at_workfile": True, "add_retime": True } node_name_template = "{class_name}_{ext}" @classmethod def get_options(cls, *args): return [ qargparse.Boolean( "start_at_workfile", help="Load at workfile start frame", default=cls.options_defaults["start_at_workfile"] ), qargparse.Boolean( "add_retime", help="Load with retime", default=cls.options_defaults["add_retime"] ) ] @classmethod def get_representations(cls): return cls._representations or cls.representations def load(self, context, name, namespace, options): """Load asset via database """ representation = context["representation"] # reset container id so it is always unique for each instance self.reset_container_id() is_sequence = len(representation["files"]) > 1 if is_sequence: context["representation"] = \ self._representation_with_hash_in_frame( representation ) filepath = self.filepath_from_context(context) filepath = filepath.replace("\\", "/") start_at_workfile = options.get( "start_at_workfile", self.options_defaults["start_at_workfile"]) add_retime = options.get( "add_retime", self.options_defaults["add_retime"]) version = context['version'] version_data = version.get("data", {}) repre_id = representation["_id"] self.log.debug("_ version_data: {}\n".format( pformat(version_data))) self.log.debug( "Representation id `{}` ".format(repre_id)) self.handle_start = version_data.get("handleStart", 0) self.handle_end = version_data.get("handleEnd", 0) first = version_data.get("frameStart", None) last = version_data.get("frameEnd", None) first -= self.handle_start last += self.handle_end if not is_sequence: duration = last - first first = 1 last = first + duration # Fallback to asset name when namespace is None if namespace is None: namespace = context['asset']['name'] if not filepath: self.log.warning( "Representation id `{}` is failing to load".format(repre_id)) return read_name = self._get_node_name(representation) # Create the Loader with the filename path set read_node = nuke.createNode( "Read", "name {}".format(read_name), inpanel=False ) # to avoid multiple undo steps for rest of process # we will switch off undo-ing with viewer_update_and_undo_stop(): read_node["file"].setValue(filepath) self.set_colorspace_to_node( read_node, filepath, version, representation) self._set_range_to_node(read_node, first, last, start_at_workfile) # add additional metadata from the version to imprint Avalon knob add_keys = ["frameStart", "frameEnd", "source", "colorspace", "author", "fps", "version", "handleStart", "handleEnd"] data_imprint = {} for key in add_keys: if key == 'version': version_doc = context["version"] if version_doc["type"] == "hero_version": version = "hero" else: version = version_doc.get("name") if version: data_imprint[key] = version elif key == 'colorspace': colorspace = representation["data"].get(key) colorspace = colorspace or version_data.get(key) data_imprint["db_colorspace"] = colorspace else: value_ = context["version"]['data'].get( key, str(None)) if isinstance(value_, (str)): value_ = value_.replace("\\", "/") data_imprint[key] = value_ if add_retime and version_data.get("retime", None): data_imprint["addRetime"] = True read_node["tile_color"].setValue(int("0x4ecd25ff", 16)) container = containerise( read_node, name=name, namespace=namespace, context=context, loader=self.__class__.__name__, data=data_imprint) if add_retime and version_data.get("retime", None): self._make_retimes(read_node, version_data) self.set_as_member(read_node) return container def switch(self, container, representation): self.update(container, representation) def _representation_with_hash_in_frame(self, representation): """Convert frame key value to padded hash Args: representation (dict): representation data Returns: dict: altered representation data """ representation = deepcopy(representation) context = representation["context"] # Get the frame from the context and hash it frame = context["frame"] hashed_frame = "#" * len(str(frame)) # Replace the frame with the hash in the originalBasename if ( "{originalBasename}" in representation["data"]["template"] ): origin_basename = context["originalBasename"] context["originalBasename"] = origin_basename.replace( frame, hashed_frame ) # Replace the frame with the hash in the frame representation["context"]["frame"] = hashed_frame return representation def update(self, container, representation): """Update the Loader's path Nuke automatically tries to reset some variables when changing the loader's path to a new file. These automatic changes are to its inputs: """ is_sequence = len(representation["files"]) > 1 read_node = container["node"] if is_sequence: representation = self._representation_with_hash_in_frame( representation ) filepath = get_representation_path(representation).replace("\\", "/") self.log.debug("_ filepath: {}".format(filepath)) start_at_workfile = "start at" in read_node['frame_mode'].value() add_retime = [ key for key in read_node.knobs().keys() if "addRetime" in key ] project_name = get_current_project_name() version_doc = get_version_by_id(project_name, representation["parent"]) version_data = version_doc.get("data", {}) repre_id = representation["_id"] # colorspace profile colorspace = representation["data"].get("colorspace") colorspace = colorspace or version_data.get("colorspace") self.handle_start = version_data.get("handleStart", 0) self.handle_end = version_data.get("handleEnd", 0) first = version_data.get("frameStart", None) last = version_data.get("frameEnd", None) first -= self.handle_start last += self.handle_end if not is_sequence: duration = last - first first = 1 last = first + duration if not filepath: self.log.warning( "Representation id `{}` is failing to load".format(repre_id)) return read_node["file"].setValue(filepath) # to avoid multiple undo steps for rest of process # we will switch off undo-ing with viewer_update_and_undo_stop(): self.set_colorspace_to_node( read_node, filepath, version_doc, representation) self._set_range_to_node(read_node, first, last, start_at_workfile) updated_dict = { "representation": str(representation["_id"]), "frameStart": str(first), "frameEnd": str(last), "version": str(version_doc.get("name")), "db_colorspace": colorspace, "source": version_data.get("source"), "handleStart": str(self.handle_start), "handleEnd": str(self.handle_end), "fps": str(version_data.get("fps")), "author": version_data.get("author") } last_version_doc = get_last_version_by_subset_id( project_name, version_doc["parent"], fields=["_id"] ) # change color of read_node if version_doc["_id"] == last_version_doc["_id"]: color_value = "0x4ecd25ff" else: color_value = "0xd84f20ff" read_node["tile_color"].setValue(int(color_value, 16)) # Update the imprinted representation update_container( read_node, updated_dict ) self.log.info( "updated to version: {}".format(version_doc.get("name")) ) if add_retime and version_data.get("retime", None): self._make_retimes(read_node, version_data) else: self.clear_members(read_node) self.set_as_member(read_node) def set_colorspace_to_node( self, read_node, filepath, version_doc, representation_doc, ): """Set colorspace to read node. Sets colorspace with available names validation. Args: read_node (nuke.Node): The nuke's read node filepath (str): file path version_doc (dict): version document representation_doc (dict): representation document """ used_colorspace = self._get_colorspace_data( version_doc, representation_doc, filepath) if ( used_colorspace and colorspace_exists_on_node(read_node, used_colorspace) ): self.log.info(f"Used colorspace: {used_colorspace}") read_node["colorspace"].setValue(used_colorspace) else: self.log.info("Colorspace not set...") def remove(self, container): read_node = container["node"] assert read_node.Class() == "Read", "Must be Read" with viewer_update_and_undo_stop(): members = self.get_members(read_node) nuke.delete(read_node) for member in members: nuke.delete(member) def _set_range_to_node(self, read_node, first, last, start_at_workfile): read_node['origfirst'].setValue(int(first)) read_node['first'].setValue(int(first)) read_node['origlast'].setValue(int(last)) read_node['last'].setValue(int(last)) # set start frame depending on workfile or version self._loader_shift(read_node, start_at_workfile) def _make_retimes(self, parent_node, version_data): ''' Create all retime and timewarping nodes with copied animation ''' speed = version_data.get('speed', 1) time_warp_nodes = version_data.get('timewarps', []) last_node = None source_id = self.get_container_id(parent_node) self.log.debug("__ source_id: {}".format(source_id)) self.log.debug("__ members: {}".format( self.get_members(parent_node))) dependent_nodes = self.clear_members(parent_node) with maintained_selection(): parent_node['selected'].setValue(True) if speed != 1: rtn = nuke.createNode( "Retime", "speed {}".format(speed)) rtn["before"].setValue("continue") rtn["after"].setValue("continue") rtn["input.first_lock"].setValue(True) rtn["input.first"].setValue( self.script_start ) self.set_as_member(rtn) last_node = rtn if time_warp_nodes != []: start_anim = self.script_start + (self.handle_start / speed) for timewarp in time_warp_nodes: twn = nuke.createNode( timewarp["Class"], "name {}".format(timewarp["name"]) ) if isinstance(timewarp["lookup"], list): # if array for animation twn["lookup"].setAnimated() for i, value in enumerate(timewarp["lookup"]): twn["lookup"].setValueAt( (start_anim + i) + value, (start_anim + i)) else: # if static value `int` twn["lookup"].setValue(timewarp["lookup"]) self.set_as_member(twn) last_node = twn if dependent_nodes: # connect to original inputs for i, n in enumerate(dependent_nodes): last_node.setInput(i, n) def _loader_shift(self, read_node, workfile_start=False): """ Set start frame of read node to a workfile start Args: read_node (nuke.Node): The nuke's read node workfile_start (bool): set workfile start frame if true """ if workfile_start: read_node['frame_mode'].setValue("start at") read_node['frame'].setValue(str(self.script_start)) def _get_node_name(self, representation): repre_cont = representation["context"] name_data = { "asset": repre_cont["asset"], "subset": repre_cont["subset"], "representation": representation["name"], "ext": repre_cont["representation"], "id": representation["_id"], "class_name": self.__class__.__name__ } return self.node_name_template.format(**name_data) def _get_colorspace_data(self, version_doc, representation_doc, filepath): """Get colorspace data from version and representation documents Args: version_doc (dict): version document representation_doc (dict): representation document filepath (str): file path Returns: Any[str,None]: colorspace name or None """ # Get backward compatible colorspace key. colorspace = representation_doc["data"].get("colorspace") self.log.debug( f"Colorspace from representation colorspace: {colorspace}" ) # Get backward compatible version data key if colorspace is not found. colorspace = colorspace or version_doc["data"].get("colorspace") self.log.debug(f"Colorspace from version colorspace: {colorspace}") # Get colorspace from representation colorspaceData if colorspace is # not found. colorspace_data = representation_doc["data"].get("colorspaceData", {}) colorspace = colorspace or colorspace_data.get("colorspace") self.log.debug( f"Colorspace from representation colorspaceData: {colorspace}" ) print(f"Colorspace found: {colorspace}") # check if any filerules are not applicable new_parsed_colorspace = get_imageio_file_rules_colorspace_from_filepath( # noqa filepath, "nuke", get_current_project_name() ) self.log.debug(f"Colorspace new filerules: {new_parsed_colorspace}") # colorspace from `project_settings/nuke/imageio/regexInputs` old_parsed_colorspace = get_imageio_input_colorspace(filepath) self.log.debug(f"Colorspace old filerules: {old_parsed_colorspace}") return ( new_parsed_colorspace or old_parsed_colorspace or colorspace ) ================================================ FILE: openpype/hosts/nuke/plugins/load/load_effects.py ================================================ import json from collections import OrderedDict import nuke import six from openpype.client import ( get_version_by_id, get_last_version_by_subset_id, ) from openpype.pipeline import ( load, get_current_project_name, get_representation_path, ) from openpype.hosts.nuke.api import ( containerise, update_container, viewer_update_and_undo_stop ) class LoadEffects(load.LoaderPlugin): """Loading colorspace soft effect exported from nukestudio""" families = ["effect"] representations = ["*"] extensions = {"json"} label = "Load Effects - nodes" order = 0 icon = "cc" color = "white" ignore_attr = ["useLifetime"] def load(self, context, name, namespace, data): """ Loading function to get the soft effects to particular read node Arguments: context (dict): context of version name (str): name of the version namespace (str): asset name data (dict): compulsory attribute > not used Returns: nuke node: containerised nuke node object """ # get main variables version = context['version'] version_data = version.get("data", {}) vname = version.get("name", None) first = version_data.get("frameStart", None) last = version_data.get("frameEnd", None) workfile_first_frame = int(nuke.root()["first_frame"].getValue()) namespace = namespace or context['asset']['name'] colorspace = version_data.get("colorspace", None) object_name = "{}_{}".format(name, namespace) # prepare data for imprinting # add additional metadata from the version to imprint to Avalon knob add_keys = ["frameStart", "frameEnd", "handleStart", "handleEnd", "source", "author", "fps"] data_imprint = { "frameStart": first, "frameEnd": last, "version": vname, "colorspaceInput": colorspace, } for k in add_keys: data_imprint.update({k: version_data[k]}) # getting file path file = self.filepath_from_context(context).replace("\\", "/") # getting data from json file with unicode conversion with open(file, "r") as f: json_f = {self.byteify(key): self.byteify(value) for key, value in json.load(f).items()} # get correct order of nodes by positions on track and subtrack nodes_order = self.reorder_nodes(json_f) # adding nodes to node graph # just in case we are in group lets jump out of it nuke.endGroup() GN = nuke.createNode( "Group", "name {}_1".format(object_name), inpanel=False ) # adding content to the group node with GN: pre_node = nuke.createNode("Input") pre_node["name"].setValue("rgb") for ef_name, ef_val in nodes_order.items(): node = nuke.createNode(ef_val["class"]) for k, v in ef_val["node"].items(): if k in self.ignore_attr: continue try: node[k].value() except NameError as e: self.log.warning(e) continue if isinstance(v, list) and len(v) > 4: node[k].setAnimated() for i, value in enumerate(v): if isinstance(value, list): for ci, cv in enumerate(value): node[k].setValueAt( cv, (workfile_first_frame + i), ci) else: node[k].setValueAt( value, (workfile_first_frame + i)) else: node[k].setValue(v) node.setInput(0, pre_node) pre_node = node output = nuke.createNode("Output") output.setInput(0, pre_node) # try to find parent read node self.connect_read_node(GN, namespace, json_f["assignTo"]) GN["tile_color"].setValue(int("0x3469ffff", 16)) self.log.info("Loaded lut setup: `{}`".format(GN["name"].value())) return containerise( node=GN, name=name, namespace=namespace, context=context, loader=self.__class__.__name__, data=data_imprint) def update(self, container, representation): """Update the Loader's path Nuke automatically tries to reset some variables when changing the loader's path to a new file. These automatic changes are to its inputs: """ # get main variables # Get version from io project_name = get_current_project_name() version_doc = get_version_by_id(project_name, representation["parent"]) # get corresponding node GN = container["node"] file = get_representation_path(representation).replace("\\", "/") name = container['name'] version_data = version_doc.get("data", {}) vname = version_doc.get("name", None) first = version_data.get("frameStart", None) last = version_data.get("frameEnd", None) workfile_first_frame = int(nuke.root()["first_frame"].getValue()) namespace = container['namespace'] colorspace = version_data.get("colorspace", None) object_name = "{}_{}".format(name, namespace) add_keys = ["frameStart", "frameEnd", "handleStart", "handleEnd", "source", "author", "fps"] data_imprint = { "representation": str(representation["_id"]), "frameStart": first, "frameEnd": last, "version": vname, "colorspaceInput": colorspace } for k in add_keys: data_imprint.update({k: version_data[k]}) # Update the imprinted representation update_container( GN, data_imprint ) # getting data from json file with unicode conversion with open(file, "r") as f: json_f = {self.byteify(key): self.byteify(value) for key, value in json.load(f).items()} # get correct order of nodes by positions on track and subtrack nodes_order = self.reorder_nodes(json_f) # adding nodes to node graph # just in case we are in group lets jump out of it nuke.endGroup() # adding content to the group node with GN: # first remove all nodes [nuke.delete(n) for n in nuke.allNodes()] # create input node pre_node = nuke.createNode("Input") pre_node["name"].setValue("rgb") for _, ef_val in nodes_order.items(): node = nuke.createNode(ef_val["class"]) for k, v in ef_val["node"].items(): if k in self.ignore_attr: continue try: node[k].value() except NameError as e: self.log.warning(e) continue if isinstance(v, list) and len(v) > 4: node[k].setAnimated() for i, value in enumerate(v): if isinstance(value, list): for ci, cv in enumerate(value): node[k].setValueAt( cv, (workfile_first_frame + i), ci) else: node[k].setValueAt( value, (workfile_first_frame + i)) else: node[k].setValue(v) node.setInput(0, pre_node) pre_node = node # create output node output = nuke.createNode("Output") output.setInput(0, pre_node) # try to find parent read node self.connect_read_node(GN, namespace, json_f["assignTo"]) last_version_doc = get_last_version_by_subset_id( project_name, version_doc["parent"], fields=["_id"] ) # change color of node if version_doc["_id"] == last_version_doc["_id"]: color_value = "0x3469ffff" else: color_value = "0xd84f20ff" GN["tile_color"].setValue(int(color_value, 16)) self.log.info("updated to version: {}".format(version_doc.get("name"))) def connect_read_node(self, group_node, asset, subset): """ Finds read node and selects it Arguments: asset (str): asset name Returns: nuke node: node is selected None: if nothing found """ search_name = "{0}_{1}".format(asset, subset) node = [ n for n in nuke.allNodes(filter="Read") if search_name in n["file"].value() ] if len(node) > 0: rn = node[0] else: rn = None # Parent read node has been found # solving connections if rn: dep_nodes = rn.dependent() if len(dep_nodes) > 0: for dn in dep_nodes: dn.setInput(0, group_node) group_node.setInput(0, rn) group_node.autoplace() def reorder_nodes(self, data): new_order = OrderedDict() trackNums = [v["trackIndex"] for k, v in data.items() if isinstance(v, dict)] subTrackNums = [v["subTrackIndex"] for k, v in data.items() if isinstance(v, dict)] for trackIndex in range( min(trackNums), max(trackNums) + 1): for subTrackIndex in range( min(subTrackNums), max(subTrackNums) + 1): item = self.get_item(data, trackIndex, subTrackIndex) if item is not {}: new_order.update(item) return new_order def get_item(self, data, trackIndex, subTrackIndex): return {key: val for key, val in data.items() if isinstance(val, dict) if subTrackIndex == val["subTrackIndex"] if trackIndex == val["trackIndex"]} def byteify(self, input): """ Converts unicode strings to strings It goes through all dictionary Arguments: input (dict/str): input Returns: dict: with fixed values and keys """ if isinstance(input, dict): return {self.byteify(key): self.byteify(value) for key, value in input.items()} elif isinstance(input, list): return [self.byteify(element) for element in input] elif isinstance(input, six.text_type): return str(input) else: return input def switch(self, container, representation): self.update(container, representation) def remove(self, container): node = container["node"] with viewer_update_and_undo_stop(): nuke.delete(node) ================================================ FILE: openpype/hosts/nuke/plugins/load/load_effects_ip.py ================================================ import json from collections import OrderedDict import six import nuke from openpype.client import ( get_version_by_id, get_last_version_by_subset_id, ) from openpype.pipeline import ( load, get_current_project_name, get_representation_path, ) from openpype.hosts.nuke.api import lib from openpype.hosts.nuke.api import ( containerise, update_container, viewer_update_and_undo_stop ) class LoadEffectsInputProcess(load.LoaderPlugin): """Loading colorspace soft effect exported from nukestudio""" families = ["effect"] representations = ["*"] extensions = {"json"} label = "Load Effects - Input Process" order = 0 icon = "eye" color = "#cc0000" ignore_attr = ["useLifetime"] def load(self, context, name, namespace, data): """ Loading function to get the soft effects to particular read node Arguments: context (dict): context of version name (str): name of the version namespace (str): asset name data (dict): compulsory attribute > not used Returns: nuke node: containerised nuke node object """ # get main variables version = context['version'] version_data = version.get("data", {}) vname = version.get("name", None) first = version_data.get("frameStart", None) last = version_data.get("frameEnd", None) workfile_first_frame = int(nuke.root()["first_frame"].getValue()) namespace = namespace or context['asset']['name'] colorspace = version_data.get("colorspace", None) object_name = "{}_{}".format(name, namespace) # prepare data for imprinting # add additional metadata from the version to imprint to Avalon knob add_keys = ["frameStart", "frameEnd", "handleStart", "handleEnd", "source", "author", "fps"] data_imprint = { "frameStart": first, "frameEnd": last, "version": vname, "colorspaceInput": colorspace, } for k in add_keys: data_imprint.update({k: version_data[k]}) # getting file path file = self.filepath_from_context(context).replace("\\", "/") # getting data from json file with unicode conversion with open(file, "r") as f: json_f = {self.byteify(key): self.byteify(value) for key, value in json.load(f).items()} # get correct order of nodes by positions on track and subtrack nodes_order = self.reorder_nodes(json_f) # adding nodes to node graph # just in case we are in group lets jump out of it nuke.endGroup() GN = nuke.createNode( "Group", "name {}_1".format(object_name), inpanel=False ) # adding content to the group node with GN: pre_node = nuke.createNode("Input") pre_node["name"].setValue("rgb") for _, ef_val in nodes_order.items(): node = nuke.createNode(ef_val["class"]) for k, v in ef_val["node"].items(): if k in self.ignore_attr: continue try: node[k].value() except NameError as e: self.log.warning(e) continue if isinstance(v, list) and len(v) > 4: node[k].setAnimated() for i, value in enumerate(v): if isinstance(value, list): for ci, cv in enumerate(value): node[k].setValueAt( cv, (workfile_first_frame + i), ci) else: node[k].setValueAt( value, (workfile_first_frame + i)) else: node[k].setValue(v) node.setInput(0, pre_node) pre_node = node output = nuke.createNode("Output") output.setInput(0, pre_node) # try to place it under Viewer1 if not self.connect_active_viewer(GN): nuke.delete(GN) return GN["tile_color"].setValue(int("0x3469ffff", 16)) self.log.info("Loaded lut setup: `{}`".format(GN["name"].value())) return containerise( node=GN, name=name, namespace=namespace, context=context, loader=self.__class__.__name__, data=data_imprint) def update(self, container, representation): """Update the Loader's path Nuke automatically tries to reset some variables when changing the loader's path to a new file. These automatic changes are to its inputs: """ # get main variables # Get version from io project_name = get_current_project_name() version_doc = get_version_by_id(project_name, representation["parent"]) # get corresponding node GN = container["node"] file = get_representation_path(representation).replace("\\", "/") version_data = version_doc.get("data", {}) vname = version_doc.get("name", None) first = version_data.get("frameStart", None) last = version_data.get("frameEnd", None) workfile_first_frame = int(nuke.root()["first_frame"].getValue()) colorspace = version_data.get("colorspace", None) add_keys = ["frameStart", "frameEnd", "handleStart", "handleEnd", "source", "author", "fps"] data_imprint = { "representation": str(representation["_id"]), "frameStart": first, "frameEnd": last, "version": vname, "colorspaceInput": colorspace, } for k in add_keys: data_imprint.update({k: version_data[k]}) # Update the imprinted representation update_container( GN, data_imprint ) # getting data from json file with unicode conversion with open(file, "r") as f: json_f = {self.byteify(key): self.byteify(value) for key, value in json.load(f).items()} # get correct order of nodes by positions on track and subtrack nodes_order = self.reorder_nodes(json_f) # adding nodes to node graph # just in case we are in group lets jump out of it nuke.endGroup() # adding content to the group node with GN: # first remove all nodes [nuke.delete(n) for n in nuke.allNodes()] # create input node pre_node = nuke.createNode("Input") pre_node["name"].setValue("rgb") for _, ef_val in nodes_order.items(): node = nuke.createNode(ef_val["class"]) for k, v in ef_val["node"].items(): if k in self.ignore_attr: continue try: node[k].value() except NameError as e: self.log.warning(e) continue if isinstance(v, list) and len(v) > 4: node[k].setAnimated() for i, value in enumerate(v): if isinstance(value, list): for ci, cv in enumerate(value): node[k].setValueAt( cv, (workfile_first_frame + i), ci) else: node[k].setValueAt( value, (workfile_first_frame + i)) else: node[k].setValue(v) node.setInput(0, pre_node) pre_node = node # create output node output = nuke.createNode("Output") output.setInput(0, pre_node) # get all versions in list last_version_doc = get_last_version_by_subset_id( project_name, version_doc["parent"], fields=["_id"] ) # change color of node if version_doc["_id"] == last_version_doc["_id"]: color_value = "0x3469ffff" else: color_value = "0xd84f20ff" GN["tile_color"].setValue(int(color_value, 16)) self.log.info("updated to version: {}".format(version_doc.get("name"))) def connect_active_viewer(self, group_node): """ Finds Active viewer and place the node under it, also adds name of group into Input Process of the viewer Arguments: group_node (nuke node): nuke group node object """ group_node_name = group_node["name"].value() viewer = [n for n in nuke.allNodes() if "Viewer1" in n["name"].value()] if len(viewer) > 0: viewer = viewer[0] else: msg = str("Please create Viewer node before you " "run this action again") self.log.error(msg) nuke.message(msg) return None # get coordinates of Viewer1 xpos = viewer["xpos"].value() ypos = viewer["ypos"].value() ypos += 150 viewer["ypos"].setValue(ypos) # set coordinates to group node group_node["xpos"].setValue(xpos) group_node["ypos"].setValue(ypos + 50) # add group node name to Viewer Input Process viewer["input_process_node"].setValue(group_node_name) # put backdrop under lib.create_backdrop( label="Input Process", layer=2, nodes=[viewer, group_node], color="0x7c7faaff") return True def reorder_nodes(self, data): new_order = OrderedDict() trackNums = [v["trackIndex"] for k, v in data.items() if isinstance(v, dict)] subTrackNums = [v["subTrackIndex"] for k, v in data.items() if isinstance(v, dict)] for trackIndex in range( min(trackNums), max(trackNums) + 1): for subTrackIndex in range( min(subTrackNums), max(subTrackNums) + 1): item = self.get_item(data, trackIndex, subTrackIndex) if item is not {}: new_order.update(item) return new_order def get_item(self, data, trackIndex, subTrackIndex): return {key: val for key, val in data.items() if isinstance(val, dict) if subTrackIndex == val["subTrackIndex"] if trackIndex == val["trackIndex"]} def byteify(self, input): """ Converts unicode strings to strings It goes through all dictionary Arguments: input (dict/str): input Returns: dict: with fixed values and keys """ if isinstance(input, dict): return {self.byteify(key): self.byteify(value) for key, value in input.items()} elif isinstance(input, list): return [self.byteify(element) for element in input] elif isinstance(input, six.text_type): return str(input) else: return input def switch(self, container, representation): self.update(container, representation) def remove(self, container): node = container["node"] with viewer_update_and_undo_stop(): nuke.delete(node) ================================================ FILE: openpype/hosts/nuke/plugins/load/load_gizmo.py ================================================ import nuke from openpype.client import ( get_version_by_id, get_last_version_by_subset_id, ) from openpype.pipeline import ( load, get_current_project_name, get_representation_path, ) from openpype.hosts.nuke.api.lib import ( maintained_selection, get_avalon_knob_data, set_avalon_knob_data, swap_node_with_dependency, ) from openpype.hosts.nuke.api import ( containerise, update_container, viewer_update_and_undo_stop ) class LoadGizmo(load.LoaderPlugin): """Loading nuke Gizmo""" families = ["gizmo", "lensDistortion"] representations = ["*"] extensions = {"nk"} label = "Load Gizmo" order = 0 icon = "dropbox" color = "white" node_color = "0x75338eff" def load(self, context, name, namespace, data): """ Loading function to get Gizmo into node graph Arguments: context (dict): context of version name (str): name of the version namespace (str): asset name data (dict): compulsory attribute > not used Returns: nuke node: containerized nuke node object """ # get main variables version = context['version'] version_data = version.get("data", {}) vname = version.get("name", None) first = version_data.get("frameStart", None) last = version_data.get("frameEnd", None) namespace = namespace or context['asset']['name'] colorspace = version_data.get("colorspace", None) object_name = "{}_{}".format(name, namespace) # prepare data for imprinting # add additional metadata from the version to imprint to Avalon knob add_keys = ["frameStart", "frameEnd", "handleStart", "handleEnd", "source", "author", "fps"] data_imprint = { "frameStart": first, "frameEnd": last, "version": vname, "colorspaceInput": colorspace } for k in add_keys: data_imprint.update({k: version_data[k]}) # getting file path file = self.filepath_from_context(context).replace("\\", "/") # adding nodes to node graph # just in case we are in group lets jump out of it nuke.endGroup() with maintained_selection(): # add group from nk nuke.nodePaste(file) group_node = nuke.selectedNode() group_node["name"].setValue(object_name) return containerise( node=group_node, name=name, namespace=namespace, context=context, loader=self.__class__.__name__, data=data_imprint) def update(self, container, representation): """Update the Loader's path Nuke automatically tries to reset some variables when changing the loader's path to a new file. These automatic changes are to its inputs: """ # get main variables # Get version from io project_name = get_current_project_name() version_doc = get_version_by_id(project_name, representation["parent"]) # get corresponding node group_node = container["node"] file = get_representation_path(representation).replace("\\", "/") name = container['name'] version_data = version_doc.get("data", {}) vname = version_doc.get("name", None) first = version_data.get("frameStart", None) last = version_data.get("frameEnd", None) namespace = container['namespace'] colorspace = version_data.get("colorspace", None) object_name = "{}_{}".format(name, namespace) add_keys = ["frameStart", "frameEnd", "handleStart", "handleEnd", "source", "author", "fps"] data_imprint = { "representation": str(representation["_id"]), "frameStart": first, "frameEnd": last, "version": vname, "colorspaceInput": colorspace } for k in add_keys: data_imprint.update({k: version_data[k]}) # capture pipeline metadata avalon_data = get_avalon_knob_data(group_node) # adding nodes to node graph # just in case we are in group lets jump out of it nuke.endGroup() with maintained_selection([group_node]): # insert nuke script to the script nuke.nodePaste(file) # convert imported to selected node new_group_node = nuke.selectedNode() # swap nodes with maintained connections with swap_node_with_dependency( group_node, new_group_node) as node_name: new_group_node["name"].setValue(node_name) # set updated pipeline metadata set_avalon_knob_data(new_group_node, avalon_data) last_version_doc = get_last_version_by_subset_id( project_name, version_doc["parent"], fields=["_id"] ) # change color of node if version_doc["_id"] == last_version_doc["_id"]: color_value = self.node_color else: color_value = "0xd88467ff" new_group_node["tile_color"].setValue(int(color_value, 16)) self.log.info("updated to version: {}".format(version_doc.get("name"))) return update_container(new_group_node, data_imprint) def switch(self, container, representation): self.update(container, representation) def remove(self, container): node = container["node"] with viewer_update_and_undo_stop(): nuke.delete(node) ================================================ FILE: openpype/hosts/nuke/plugins/load/load_gizmo_ip.py ================================================ import nuke import six from openpype.client import ( get_version_by_id, get_last_version_by_subset_id, ) from openpype.pipeline import ( load, get_current_project_name, get_representation_path, ) from openpype.hosts.nuke.api.lib import ( maintained_selection, create_backdrop, get_avalon_knob_data, set_avalon_knob_data, swap_node_with_dependency, ) from openpype.hosts.nuke.api import ( containerise, update_container, viewer_update_and_undo_stop ) class LoadGizmoInputProcess(load.LoaderPlugin): """Loading colorspace soft effect exported from nukestudio""" families = ["gizmo"] representations = ["*"] extensions = {"nk"} label = "Load Gizmo - Input Process" order = 0 icon = "eye" color = "#cc0000" node_color = "0x7533c1ff" def load(self, context, name, namespace, data): """ Loading function to get Gizmo as Input Process on viewer Arguments: context (dict): context of version name (str): name of the version namespace (str): asset name data (dict): compulsory attribute > not used Returns: nuke node: containerized nuke node object """ # get main variables version = context['version'] version_data = version.get("data", {}) vname = version.get("name", None) first = version_data.get("frameStart", None) last = version_data.get("frameEnd", None) namespace = namespace or context['asset']['name'] colorspace = version_data.get("colorspace", None) object_name = "{}_{}".format(name, namespace) # prepare data for imprinting # add additional metadata from the version to imprint to Avalon knob add_keys = ["frameStart", "frameEnd", "handleStart", "handleEnd", "source", "author", "fps"] data_imprint = { "frameStart": first, "frameEnd": last, "version": vname, "colorspaceInput": colorspace } for k in add_keys: data_imprint.update({k: version_data[k]}) # getting file path file = self.filepath_from_context(context).replace("\\", "/") # adding nodes to node graph # just in case we are in group lets jump out of it nuke.endGroup() with maintained_selection(): # add group from nk nuke.nodePaste(file) group_node = nuke.selectedNode() group_node["name"].setValue(object_name) # try to place it under Viewer1 if not self.connect_active_viewer(group_node): nuke.delete(group_node) return return containerise( node=group_node, name=name, namespace=namespace, context=context, loader=self.__class__.__name__, data=data_imprint) def update(self, container, representation): """Update the Loader's path Nuke automatically tries to reset some variables when changing the loader's path to a new file. These automatic changes are to its inputs: """ # get main variables # Get version from io project_name = get_current_project_name() version_doc = get_version_by_id(project_name, representation["parent"]) # get corresponding node group_node = container["node"] file = get_representation_path(representation).replace("\\", "/") name = container['name'] version_data = version_doc.get("data", {}) vname = version_doc.get("name", None) first = version_data.get("frameStart", None) last = version_data.get("frameEnd", None) namespace = container['namespace'] colorspace = version_data.get("colorspace", None) object_name = "{}_{}".format(name, namespace) add_keys = ["frameStart", "frameEnd", "handleStart", "handleEnd", "source", "author", "fps"] data_imprint = { "representation": str(representation["_id"]), "frameStart": first, "frameEnd": last, "version": vname, "colorspaceInput": colorspace } for k in add_keys: data_imprint.update({k: version_data[k]}) # capture pipeline metadata avalon_data = get_avalon_knob_data(group_node) # adding nodes to node graph # just in case we are in group lets jump out of it nuke.endGroup() with maintained_selection([group_node]): # insert nuke script to the script nuke.nodePaste(file) # convert imported to selected node new_group_node = nuke.selectedNode() # swap nodes with maintained connections with swap_node_with_dependency( group_node, new_group_node) as node_name: new_group_node["name"].setValue(node_name) # set updated pipeline metadata set_avalon_knob_data(new_group_node, avalon_data) last_version_doc = get_last_version_by_subset_id( project_name, version_doc["parent"], fields=["_id"] ) # change color of node if version_doc["_id"] == last_version_doc["_id"]: color_value = self.node_color else: color_value = "0xd88467ff" new_group_node["tile_color"].setValue(int(color_value, 16)) self.log.info("updated to version: {}".format(version_doc.get("name"))) return update_container(new_group_node, data_imprint) def connect_active_viewer(self, group_node): """ Finds Active viewer and place the node under it, also adds name of group into Input Process of the viewer Arguments: group_node (nuke node): nuke group node object """ group_node_name = group_node["name"].value() viewer = [n for n in nuke.allNodes() if "Viewer1" in n["name"].value()] if len(viewer) > 0: viewer = viewer[0] else: msg = str("Please create Viewer node before you " "run this action again") self.log.error(msg) nuke.message(msg) return None # get coordinates of Viewer1 xpos = viewer["xpos"].value() ypos = viewer["ypos"].value() ypos += 150 viewer["ypos"].setValue(ypos) # set coordinates to group node group_node["xpos"].setValue(xpos) group_node["ypos"].setValue(ypos + 50) # add group node name to Viewer Input Process viewer["input_process_node"].setValue(group_node_name) # put backdrop under create_backdrop( label="Input Process", layer=2, nodes=[viewer, group_node], color="0x7c7faaff" ) return True def get_item(self, data, trackIndex, subTrackIndex): return {key: val for key, val in data.items() if subTrackIndex == val["subTrackIndex"] if trackIndex == val["trackIndex"]} def byteify(self, input): """ Converts unicode strings to strings It goes through all dictionary Arguments: input (dict/str): input Returns: dict: with fixed values and keys """ if isinstance(input, dict): return {self.byteify(key): self.byteify(value) for key, value in input.items()} elif isinstance(input, list): return [self.byteify(element) for element in input] elif isinstance(input, six.text_type): return str(input) else: return input def switch(self, container, representation): self.update(container, representation) def remove(self, container): node = container["node"] with viewer_update_and_undo_stop(): nuke.delete(node) ================================================ FILE: openpype/hosts/nuke/plugins/load/load_image.py ================================================ import nuke import qargparse from openpype.client import ( get_version_by_id, get_last_version_by_subset_id, ) from openpype.pipeline import ( load, get_current_project_name, get_representation_path, ) from openpype.hosts.nuke.api.lib import ( get_imageio_input_colorspace ) from openpype.hosts.nuke.api import ( containerise, update_container, viewer_update_and_undo_stop ) from openpype.lib.transcoding import ( IMAGE_EXTENSIONS ) class LoadImage(load.LoaderPlugin): """Load still image into Nuke""" families = [ "render2d", "source", "plate", "render", "prerender", "review", "image" ] representations = ["*"] extensions = set( ext.lstrip(".") for ext in IMAGE_EXTENSIONS ) label = "Load Image" order = -10 icon = "image" color = "white" # Loaded from settings _representations = [] node_name_template = "{class_name}_{ext}" options = [ qargparse.Integer( "frame_number", label="Frame Number", default=int(nuke.root()["first_frame"].getValue()), min=1, max=999999, help="What frame is reading from?" ) ] @classmethod def get_representations(cls): return cls._representations or cls.representations def load(self, context, name, namespace, options): self.log.info("__ options: `{}`".format(options)) frame_number = options.get( "frame_number", int(nuke.root()["first_frame"].getValue()) ) version = context['version'] version_data = version.get("data", {}) repr_id = context["representation"]["_id"] self.log.info("version_data: {}\n".format(version_data)) self.log.debug( "Representation id `{}` ".format(repr_id)) last = first = int(frame_number) # Fallback to asset name when namespace is None if namespace is None: namespace = context['asset']['name'] file = self.filepath_from_context(context) if not file: repr_id = context["representation"]["_id"] self.log.warning( "Representation id `{}` is failing to load".format(repr_id)) return file = file.replace("\\", "/") representation = context["representation"] repr_cont = representation["context"] frame = repr_cont.get("frame") if frame: padding = len(frame) file = file.replace( frame, format(frame_number, "0{}".format(padding))) read_name = self._get_node_name(representation) # Create the Loader with the filename path set with viewer_update_and_undo_stop(): r = nuke.createNode( "Read", "name {}".format(read_name), inpanel=False ) r["file"].setValue(file) # Set colorspace defined in version data colorspace = context["version"]["data"].get("colorspace") if colorspace: r["colorspace"].setValue(str(colorspace)) preset_clrsp = get_imageio_input_colorspace(file) if preset_clrsp is not None: r["colorspace"].setValue(preset_clrsp) r["origfirst"].setValue(first) r["first"].setValue(first) r["origlast"].setValue(last) r["last"].setValue(last) # add additional metadata from the version to imprint Avalon knob add_keys = ["source", "colorspace", "author", "fps", "version"] data_imprint = { "frameStart": first, "frameEnd": last } for k in add_keys: if k == 'version': data_imprint.update({k: context["version"]['name']}) else: data_imprint.update( {k: context["version"]['data'].get(k, str(None))}) r["tile_color"].setValue(int("0x4ecd25ff", 16)) return containerise(r, name=name, namespace=namespace, context=context, loader=self.__class__.__name__, data=data_imprint) def switch(self, container, representation): self.update(container, representation) def update(self, container, representation): """Update the Loader's path Nuke automatically tries to reset some variables when changing the loader's path to a new file. These automatic changes are to its inputs: """ node = container["node"] frame_number = node["first"].value() assert node.Class() == "Read", "Must be Read" repr_cont = representation["context"] file = get_representation_path(representation) if not file: repr_id = representation["_id"] self.log.warning( "Representation id `{}` is failing to load".format(repr_id)) return file = file.replace("\\", "/") frame = repr_cont.get("frame") if frame: padding = len(frame) file = file.replace( frame, format(frame_number, "0{}".format(padding))) # Get start frame from version data project_name = get_current_project_name() version_doc = get_version_by_id(project_name, representation["parent"]) last_version_doc = get_last_version_by_subset_id( project_name, version_doc["parent"], fields=["_id"] ) version_data = version_doc.get("data", {}) last = first = int(frame_number) # Set the global in to the start frame of the sequence node["file"].setValue(file) node["origfirst"].setValue(first) node["first"].setValue(first) node["origlast"].setValue(last) node["last"].setValue(last) updated_dict = {} updated_dict.update({ "representation": str(representation["_id"]), "frameStart": str(first), "frameEnd": str(last), "version": str(version_doc.get("name")), "colorspace": version_data.get("colorspace"), "source": version_data.get("source"), "fps": str(version_data.get("fps")), "author": version_data.get("author") }) # change color of node if version_doc["_id"] == last_version_doc["_id"]: color_value = "0x4ecd25ff" else: color_value = "0xd84f20ff" node["tile_color"].setValue(int(color_value, 16)) # Update the imprinted representation update_container( node, updated_dict ) self.log.info("updated to version: {}".format(version_doc.get("name"))) def remove(self, container): node = container["node"] assert node.Class() == "Read", "Must be Read" with viewer_update_and_undo_stop(): nuke.delete(node) def _get_node_name(self, representation): repre_cont = representation["context"] name_data = { "asset": repre_cont["asset"], "subset": repre_cont["subset"], "representation": representation["name"], "ext": repre_cont["representation"], "id": representation["_id"], "class_name": self.__class__.__name__ } return self.node_name_template.format(**name_data) ================================================ FILE: openpype/hosts/nuke/plugins/load/load_matchmove.py ================================================ import nuke from openpype.pipeline import load class MatchmoveLoader(load.LoaderPlugin): """ This will run matchmove script to create track in script. """ families = ["matchmove"] representations = ["*"] extensions = {"py"} defaults = ["Camera", "Object"] label = "Run matchmove script" icon = "empire" color = "orange" def load(self, context, name, namespace, data): path = self.filepath_from_context(context) if path.lower().endswith(".py"): exec(open(path).read()) else: msg = "Unsupported script type" self.log.error(msg) nuke.message(msg) return True ================================================ FILE: openpype/hosts/nuke/plugins/load/load_model.py ================================================ import nuke from openpype.client import ( get_version_by_id, get_last_version_by_subset_id, ) from openpype.pipeline import ( load, get_current_project_name, get_representation_path, ) from openpype.hosts.nuke.api.lib import maintained_selection from openpype.hosts.nuke.api import ( containerise, update_container, viewer_update_and_undo_stop ) class AlembicModelLoader(load.LoaderPlugin): """ This will load alembic model or anim into script. """ families = ["model", "pointcache", "animation"] representations = ["*"] extensions = {"abc"} label = "Load Alembic" icon = "cube" color = "orange" node_color = "0x4ecd91ff" def load(self, context, name, namespace, data): # get main variables version = context['version'] version_data = version.get("data", {}) vname = version.get("name", None) first = version_data.get("frameStart", None) last = version_data.get("frameEnd", None) fps = version_data.get("fps") or nuke.root()["fps"].getValue() namespace = namespace or context['asset']['name'] object_name = "{}_{}".format(name, namespace) # prepare data for imprinting # add additional metadata from the version to imprint to Avalon knob add_keys = ["source", "author", "fps"] data_imprint = { "frameStart": first, "frameEnd": last, "version": vname } for k in add_keys: data_imprint.update({k: version_data[k]}) # getting file path file = self.filepath_from_context(context).replace("\\", "/") with maintained_selection(): model_node = nuke.createNode( "ReadGeo2", "name {} file {} ".format( object_name, file), inpanel=False ) model_node.forceValidate() # Ensure all items are imported and selected. scene_view = model_node.knob('scene_view') scene_view.setImportedItems(scene_view.getAllItems()) scene_view.setSelectedItems(scene_view.getAllItems()) model_node["frame_rate"].setValue(float(fps)) # workaround because nuke's bug is not adding # animation keys properly xpos = model_node.xpos() ypos = model_node.ypos() nuke.nodeCopy("%clipboard%") nuke.delete(model_node) nuke.nodePaste("%clipboard%") model_node = nuke.toNode(object_name) model_node.setXYpos(xpos, ypos) # color node by correct color by actual version self.node_version_color(version, model_node) return containerise( node=model_node, name=name, namespace=namespace, context=context, loader=self.__class__.__name__, data=data_imprint) def update(self, container, representation): """ Called by Scene Inventory when look should be updated to current version. If any reference edits cannot be applied, eg. shader renamed and material not present, reference is unloaded and cleaned. All failed edits are highlighted to the user via message box. Args: container: object that has look to be updated representation: (dict): relationship data to get proper representation from DB and persisted data in .json Returns: None """ # Get version from io project_name = get_current_project_name() version_doc = get_version_by_id(project_name, representation["parent"]) # get corresponding node model_node = container["node"] # get main variables version_data = version_doc.get("data", {}) vname = version_doc.get("name", None) first = version_data.get("frameStart", None) last = version_data.get("frameEnd", None) fps = version_data.get("fps") or nuke.root()["fps"].getValue() # prepare data for imprinting # add additional metadata from the version to imprint to Avalon knob add_keys = ["source", "author", "fps"] data_imprint = { "representation": str(representation["_id"]), "frameStart": first, "frameEnd": last, "version": vname } for k in add_keys: data_imprint.update({k: version_data[k]}) # getting file path file = get_representation_path(representation).replace("\\", "/") with maintained_selection(): model_node['selected'].setValue(True) # collect input output dependencies dependencies = model_node.dependencies() dependent = model_node.dependent() model_node["frame_rate"].setValue(float(fps)) model_node["file"].setValue(file) # Ensure all items are imported and selected. scene_view = model_node.knob('scene_view') scene_view.setImportedItems(scene_view.getAllItems()) scene_view.setSelectedItems(scene_view.getAllItems()) # workaround because nuke's bug is # not adding animation keys properly xpos = model_node.xpos() ypos = model_node.ypos() nuke.nodeCopy("%clipboard%") nuke.delete(model_node) # paste the node back and set the position nuke.nodePaste("%clipboard%") model_node = nuke.selectedNode() model_node.setXYpos(xpos, ypos) # link to original input nodes for i, input in enumerate(dependencies): model_node.setInput(i, input) # link to original output nodes for d in dependent: index = next((i for i, dpcy in enumerate( d.dependencies()) if model_node is dpcy), 0) d.setInput(index, model_node) # color node by correct color by actual version self.node_version_color(version_doc, model_node) self.log.info("updated to version: {}".format(version_doc.get("name"))) return update_container(model_node, data_imprint) def node_version_color(self, version, node): """ Coloring a node by correct color by actual version""" project_name = get_current_project_name() last_version_doc = get_last_version_by_subset_id( project_name, version["parent"], fields=["_id"] ) # change color of node if version["_id"] == last_version_doc["_id"]: color_value = self.node_color else: color_value = "0xd88467ff" node["tile_color"].setValue(int(color_value, 16)) def switch(self, container, representation): self.update(container, representation) def remove(self, container): node = nuke.toNode(container['objectName']) with viewer_update_and_undo_stop(): nuke.delete(node) ================================================ FILE: openpype/hosts/nuke/plugins/load/load_ociolook.py ================================================ import os import json import secrets import nuke import six from openpype.client import ( get_version_by_id, get_last_version_by_subset_id ) from openpype.pipeline import ( load, get_current_project_name, get_representation_path, ) from openpype.hosts.nuke.api import ( containerise, viewer_update_and_undo_stop, update_container, ) class LoadOcioLookNodes(load.LoaderPlugin): """Loading Ocio look to the nuke.Node graph""" families = ["ociolook"] representations = ["*"] extensions = {"json"} label = "Load OcioLook [nodes]" order = 0 icon = "cc" color = "white" ignore_attr = ["useLifetime"] # plugin attributes current_node_color = "0x4ecd91ff" old_node_color = "0xd88467ff" # json file variables schema_version = 1 def load(self, context, name, namespace, data): """ Loading function to get the soft effects to particular read node Arguments: context (dict): context of version name (str): name of the version namespace (str): asset name data (dict): compulsory attribute > not used Returns: nuke.Node: containerized nuke.Node object """ namespace = namespace or context['asset']['name'] suffix = secrets.token_hex(nbytes=4) node_name = "{}_{}_{}".format( name, namespace, suffix) # getting file path filepath = self.filepath_from_context(context) json_f = self._load_json_data(filepath) group_node = self._create_group_node( filepath, json_f["data"]) # renaming group node group_node["name"].setValue(node_name) self._node_version_color(context["version"], group_node) self.log.info( "Loaded lut setup: `{}`".format(group_node["name"].value())) return containerise( node=group_node, name=name, namespace=namespace, context=context, loader=self.__class__.__name__ ) def _create_group_node( self, filepath, data, group_node=None ): """Creates group node with all the nodes inside. Creating mainly `OCIOFileTransform` nodes with `OCIOColorSpace` nodes in between - in case those are needed. Arguments: filepath (str): path to json file data (dict): data from json file group_node (Optional[nuke.Node]): group node or None Returns: nuke.Node: group node with all the nodes inside """ # get corresponding node root_working_colorspace = nuke.root()["workingSpaceLUT"].value() dir_path = os.path.dirname(filepath) all_files = os.listdir(dir_path) ocio_working_colorspace = _colorspace_name_by_type( data["ocioLookWorkingSpace"]) # adding nodes to node graph # just in case we are in group lets jump out of it nuke.endGroup() input_node = None output_node = None if group_node: # remove all nodes between Input and Output nodes for node in group_node.nodes(): if node.Class() not in ["Input", "Output"]: nuke.delete(node) elif node.Class() == "Input": input_node = node elif node.Class() == "Output": output_node = node else: group_node = nuke.createNode( "Group", inpanel=False ) # adding content to the group node with group_node: pre_colorspace = root_working_colorspace # reusing input node if it exists during update if input_node: pre_node = input_node else: pre_node = nuke.createNode("Input") pre_node["name"].setValue("rgb") # Compare script working colorspace with ocio working colorspace # found in json file and convert to json's if needed if pre_colorspace != ocio_working_colorspace: pre_node = _add_ocio_colorspace_node( pre_node, pre_colorspace, ocio_working_colorspace ) pre_colorspace = ocio_working_colorspace for ocio_item in data["ocioLookItems"]: input_space = _colorspace_name_by_type( ocio_item["input_colorspace"]) output_space = _colorspace_name_by_type( ocio_item["output_colorspace"]) # making sure we are set to correct colorspace for otio item if pre_colorspace != input_space: pre_node = _add_ocio_colorspace_node( pre_node, pre_colorspace, input_space ) node = nuke.createNode("OCIOFileTransform") # file path from lut representation extension = ocio_item["ext"] item_name = ocio_item["name"] item_lut_file = next( ( file for file in all_files if file.endswith(extension) ), None ) if not item_lut_file: raise ValueError( "File with extension '{}' not " "found in directory".format(extension) ) item_lut_path = os.path.join( dir_path, item_lut_file).replace("\\", "/") node["file"].setValue(item_lut_path) node["name"].setValue(item_name) node["direction"].setValue(ocio_item["direction"]) node["interpolation"].setValue(ocio_item["interpolation"]) node["working_space"].setValue(input_space) pre_node.autoplace() node.setInput(0, pre_node) node.autoplace() # pass output space into pre_colorspace for next iteration # or for output node comparison pre_colorspace = output_space pre_node = node # making sure we are back in script working colorspace if pre_colorspace != root_working_colorspace: pre_node = _add_ocio_colorspace_node( pre_node, pre_colorspace, root_working_colorspace ) # reusing output node if it exists during update if not output_node: output = nuke.createNode("Output") else: output = output_node output.setInput(0, pre_node) return group_node def update(self, container, representation): project_name = get_current_project_name() version_doc = get_version_by_id(project_name, representation["parent"]) group_node = container["node"] filepath = get_representation_path(representation) json_f = self._load_json_data(filepath) group_node = self._create_group_node( filepath, json_f["data"], group_node ) self._node_version_color(version_doc, group_node) self.log.info("Updated lut setup: `{}`".format( group_node["name"].value())) return update_container( group_node, {"representation": str(representation["_id"])}) def _load_json_data(self, filepath): # getting data from json file with unicode conversion with open(filepath, "r") as _file: json_f = {self._bytify(key): self._bytify(value) for key, value in json.load(_file).items()} # check if the version in json_f is the same as plugin version if json_f["version"] != self.schema_version: raise KeyError( "Version of json file is not the same as plugin version") return json_f def _bytify(self, input): """ Converts unicode strings to strings It goes through all dictionary Arguments: input (dict/str): input Returns: dict: with fixed values and keys """ if isinstance(input, dict): return {self._bytify(key): self._bytify(value) for key, value in input.items()} elif isinstance(input, list): return [self._bytify(element) for element in input] elif isinstance(input, six.text_type): return str(input) else: return input def switch(self, container, representation): self.update(container, representation) def remove(self, container): node = nuke.toNode(container['objectName']) with viewer_update_and_undo_stop(): nuke.delete(node) def _node_version_color(self, version, node): """ Coloring a node by correct color by actual version""" project_name = get_current_project_name() last_version_doc = get_last_version_by_subset_id( project_name, version["parent"], fields=["_id"] ) # change color of node if version["_id"] == last_version_doc["_id"]: color_value = self.current_node_color else: color_value = self.old_node_color node["tile_color"].setValue(int(color_value, 16)) def _colorspace_name_by_type(colorspace_data): """ Returns colorspace name by type Arguments: colorspace_data (dict): colorspace data Returns: str: colorspace name """ if colorspace_data["type"] == "colorspaces": return colorspace_data["name"] elif colorspace_data["type"] == "roles": return colorspace_data["colorspace"] else: raise KeyError("Unknown colorspace type: {}".format( colorspace_data["type"])) def _add_ocio_colorspace_node(pre_node, input_space, output_space): """ Adds OCIOColorSpace node to the node graph Arguments: pre_node (nuke.Node): node to connect to input_space (str): input colorspace output_space (str): output colorspace Returns: nuke.Node: node with OCIOColorSpace node """ node = nuke.createNode("OCIOColorSpace") node.setInput(0, pre_node) node["in_colorspace"].setValue(input_space) node["out_colorspace"].setValue(output_space) pre_node.autoplace() node.setInput(0, pre_node) node.autoplace() return node ================================================ FILE: openpype/hosts/nuke/plugins/load/load_script_precomp.py ================================================ import nuke from openpype.client import ( get_version_by_id, get_last_version_by_subset_id, ) from openpype.pipeline import ( get_current_project_name, load, get_representation_path, ) from openpype.hosts.nuke.api.lib import get_avalon_knob_data from openpype.hosts.nuke.api import ( containerise, update_container, viewer_update_and_undo_stop ) class LinkAsGroup(load.LoaderPlugin): """Copy the published file to be pasted at the desired location""" families = ["workfile", "nukenodes"] representations = ["*"] extensions = {"nk"} label = "Load Precomp" order = 0 icon = "file" color = "#cc0000" def load(self, context, name, namespace, data): # for k, v in context.items(): # log.info("key: `{}`, value: {}\n".format(k, v)) version = context['version'] version_data = version.get("data", {}) vname = version.get("name", None) first = version_data.get("frameStart", None) last = version_data.get("frameEnd", None) # Fallback to asset name when namespace is None if namespace is None: namespace = context['asset']['name'] file = self.filepath_from_context(context).replace("\\", "/") self.log.info("file: {}\n".format(file)) self.log.info("versionData: {}\n".format(context["version"]["data"])) # add additional metadata from the version to imprint to Avalon knob add_keys = ["frameStart", "frameEnd", "handleStart", "handleEnd", "source", "author", "fps"] data_imprint = { "startingFrame": first, "frameStart": first, "frameEnd": last, "version": vname } for k in add_keys: data_imprint.update({k: context["version"]['data'][k]}) # group context is set to precomp, so back up one level. nuke.endGroup() # P = nuke.nodes.LiveGroup("file {}".format(file)) P = nuke.createNode( "Precomp", "file {}".format(file), inpanel=False ) # Set colorspace defined in version data colorspace = context["version"]["data"].get("colorspace", None) self.log.info("colorspace: {}\n".format(colorspace)) P["name"].setValue("{}_{}".format(name, namespace)) P["useOutput"].setValue(True) with P: # iterate through all nodes in group node and find pype writes writes = [n.name() for n in nuke.allNodes() if n.Class() == "Group" if get_avalon_knob_data(n)] if writes: # create panel for selecting output panel_choices = " ".join(writes) panel_label = "Select write node for output" p = nuke.Panel("Select Write Node") p.addEnumerationPulldown( panel_label, panel_choices) p.show() P["output"].setValue(p.value(panel_label)) P["tile_color"].setValue(0xff0ff0ff) return containerise( node=P, name=name, namespace=namespace, context=context, loader=self.__class__.__name__, data=data_imprint) def switch(self, container, representation): self.update(container, representation) def update(self, container, representation): """Update the Loader's path Nuke automatically tries to reset some variables when changing the loader's path to a new file. These automatic changes are to its inputs: """ node = container["node"] root = get_representation_path(representation).replace("\\", "/") # Get start frame from version data project_name = get_current_project_name() version_doc = get_version_by_id(project_name, representation["parent"]) last_version_doc = get_last_version_by_subset_id( project_name, version_doc["parent"], fields=["_id"] ) updated_dict = {} version_data = version_doc["data"] updated_dict.update({ "representation": str(representation["_id"]), "frameEnd": version_data.get("frameEnd"), "version": version_doc.get("name"), "colorspace": version_data.get("colorspace"), "source": version_data.get("source"), "fps": version_data.get("fps"), "author": version_data.get("author") }) # Update the imprinted representation update_container( node, updated_dict ) node["file"].setValue(root) # change color of node if version_doc["_id"] == last_version_doc["_id"]: color_value = "0xff0ff0ff" else: color_value = "0xd84f20ff" node["tile_color"].setValue(int(color_value, 16)) self.log.info("updated to version: {}".format(version_doc.get("name"))) def remove(self, container): node = container["node"] with viewer_update_and_undo_stop(): nuke.delete(node) ================================================ FILE: openpype/hosts/nuke/plugins/publish/collect_backdrop.py ================================================ from pprint import pformat import pyblish.api from openpype.hosts.nuke.api import lib as pnlib import nuke class CollectBackdrops(pyblish.api.InstancePlugin): """Collect Backdrop node instance and its content """ order = pyblish.api.CollectorOrder + 0.22 label = "Collect Backdrop" hosts = ["nuke"] families = ["nukenodes"] def process(self, instance): self.log.debug(pformat(instance.data)) bckn = instance.data["transientData"]["node"] # define size of the backdrop left = bckn.xpos() top = bckn.ypos() right = left + bckn['bdwidth'].value() bottom = top + bckn['bdheight'].value() instance.data["transientData"]["childNodes"] = [] # iterate all nodes for node in nuke.allNodes(): # exclude viewer if node.Class() == "Viewer": continue # find all related nodes if (node.xpos() > left) \ and (node.xpos() + node.screenWidth() < right) \ and (node.ypos() > top) \ and (node.ypos() + node.screenHeight() < bottom): # add contained nodes to instance's node list instance.data["transientData"]["childNodes"].append(node) # get all connections from outside of backdrop nodes = instance.data["transientData"]["childNodes"] connections_in, connections_out = pnlib.get_dependent_nodes(nodes) instance.data["transientData"]["nodeConnectionsIn"] = connections_in instance.data["transientData"]["nodeConnectionsOut"] = connections_out # make label nicer instance.data["label"] = "{0} ({1} nodes)".format( bckn.name(), len(instance.data["transientData"]["childNodes"])) # get version version = instance.context.data.get('version') if version: instance.data['version'] = version self.log.debug("Backdrop instance collected: `{}`".format(instance)) ================================================ FILE: openpype/hosts/nuke/plugins/publish/collect_context_data.py ================================================ import os import nuke import pyblish.api from openpype.lib import get_version_from_path import openpype.hosts.nuke.api as napi from openpype.pipeline import KnownPublishError class CollectContextData(pyblish.api.ContextPlugin): """Collect current context publish.""" order = pyblish.api.CollectorOrder - 0.499 label = "Collect context data" hosts = ['nuke'] def process(self, context): # sourcery skip: avoid-builtin-shadow root_node = nuke.root() current_file = os.path.normpath(root_node.name()) if current_file.lower() == "root": raise KnownPublishError( "Workfile is not correct file name. \n" "Use workfile tool to manage the name correctly." ) # Get frame range first_frame = int(root_node["first_frame"].getValue()) last_frame = int(root_node["last_frame"].getValue()) # get instance data from root root_instance_context = napi.get_node_data( root_node, napi.INSTANCE_DATA_KNOB ) handle_start = root_instance_context["handleStart"] handle_end = root_instance_context["handleEnd"] # Get format format = root_node['format'].value() resolution_width = format.width() resolution_height = format.height() pixel_aspect = format.pixelAspect() script_data = { "frameStart": first_frame + handle_start, "frameEnd": last_frame - handle_end, "resolutionWidth": resolution_width, "resolutionHeight": resolution_height, "pixelAspect": pixel_aspect, "handleStart": handle_start, "handleEnd": handle_end, "step": 1, "fps": root_node['fps'].value(), "currentFile": current_file, "version": int(get_version_from_path(current_file)), "host": pyblish.api.current_host(), "hostVersion": nuke.NUKE_VERSION_STRING } context.data["scriptData"] = script_data context.data.update(script_data) self.log.debug('Context from Nuke script collected') ================================================ FILE: openpype/hosts/nuke/plugins/publish/collect_framerate.py ================================================ import nuke import pyblish.api class CollectFramerate(pyblish.api.ContextPlugin): """Collect framerate.""" order = pyblish.api.CollectorOrder label = "Collect Framerate" hosts = [ "nuke", "nukeassist" ] def process(self, context): context.data["fps"] = nuke.root()["fps"].getValue() ================================================ FILE: openpype/hosts/nuke/plugins/publish/collect_gizmo.py ================================================ import pyblish.api import nuke class CollectGizmo(pyblish.api.InstancePlugin): """Collect Gizmo (group) node instance and its content """ order = pyblish.api.CollectorOrder + 0.22 label = "Collect Gizmo (group)" hosts = ["nuke"] families = ["gizmo"] def process(self, instance): gizmo_node = instance.data["transientData"]["node"] # add family to familiess instance.data["families"].insert(0, instance.data["family"]) # make label nicer instance.data["label"] = gizmo_node.name() # Get frame range handle_start = instance.context.data["handleStart"] handle_end = instance.context.data["handleEnd"] first_frame = int(nuke.root()["first_frame"].getValue()) last_frame = int(nuke.root()["last_frame"].getValue()) # Add version data to instance version_data = { "handleStart": handle_start, "handleEnd": handle_end, "frameStart": first_frame + handle_start, "frameEnd": last_frame - handle_end, "colorspace": nuke.root().knob('workingSpaceLUT').value(), "families": [instance.data["family"]] + instance.data["families"], "subset": instance.data["subset"], "fps": instance.context.data["fps"] } instance.data.update({ "versionData": version_data, "frameStart": first_frame, "frameEnd": last_frame }) self.log.debug("Gizmo instance collected: `{}`".format(instance)) ================================================ FILE: openpype/hosts/nuke/plugins/publish/collect_model.py ================================================ import pyblish.api import nuke class CollectModel(pyblish.api.InstancePlugin): """Collect Model node instance and its content """ order = pyblish.api.CollectorOrder + 0.22 label = "Collect Model" hosts = ["nuke"] families = ["model"] def process(self, instance): geo_node = instance.data["transientData"]["node"] # add family to familiess instance.data["families"].insert(0, instance.data["family"]) # make label nicer instance.data["label"] = geo_node.name() # Get frame range handle_start = instance.context.data["handleStart"] handle_end = instance.context.data["handleEnd"] first_frame = int(nuke.root()["first_frame"].getValue()) last_frame = int(nuke.root()["last_frame"].getValue()) # Add version data to instance version_data = { "handleStart": handle_start, "handleEnd": handle_end, "frameStart": first_frame + handle_start, "frameEnd": last_frame - handle_end, "colorspace": nuke.root().knob('workingSpaceLUT').value(), "families": [instance.data["family"]] + instance.data["families"], "subset": instance.data["subset"], "fps": instance.context.data["fps"] } instance.data.update({ "versionData": version_data, "frameStart": first_frame, "frameEnd": last_frame }) self.log.debug("Model instance collected: `{}`".format(instance)) ================================================ FILE: openpype/hosts/nuke/plugins/publish/collect_nuke_instance_data.py ================================================ import nuke import pyblish.api class CollectInstanceData(pyblish.api.InstancePlugin): """Collect Nuke instance data """ order = pyblish.api.CollectorOrder - 0.49 label = "Collect Nuke Instance Data" hosts = ["nuke", "nukeassist"] # presets sync_workfile_version_on_families = [] def process(self, instance): family = instance.data["family"] # Get format root = nuke.root() format_ = root['format'].value() resolution_width = format_.width() resolution_height = format_.height() pixel_aspect = format_.pixelAspect() # sync workfile version if family in self.sync_workfile_version_on_families: self.log.debug( "Syncing version with workfile for '{}'".format( family ) ) # get version to instance for integration instance.data['version'] = instance.context.data['version'] instance.data.update({ "step": 1, "fps": root['fps'].value(), "resolutionWidth": resolution_width, "resolutionHeight": resolution_height, "pixelAspect": pixel_aspect }) # add creator attributes to instance creator_attributes = instance.data["creator_attributes"] instance.data.update(creator_attributes) # add review family if review activated on instance if instance.data.get("review"): instance.data["families"].append("review") self.log.debug("Collected instance: {}".format( instance.data)) ================================================ FILE: openpype/hosts/nuke/plugins/publish/collect_reads.py ================================================ import os import re import nuke import pyblish.api class CollectNukeReads(pyblish.api.InstancePlugin): """Collect all read nodes.""" order = pyblish.api.CollectorOrder + 0.04 label = "Collect Source Reads" hosts = ["nuke", "nukeassist"] families = ["source"] def process(self, instance): self.log.debug("checking instance: {}".format(instance)) node = instance.data["transientData"]["node"] if node.Class() != "Read": return file_path = node["file"].value() file_name = os.path.basename(file_path) items = file_name.split(".") if len(items) < 2: raise ValueError ext = items[-1] # Get frame range handle_start = instance.context.data["handleStart"] handle_end = instance.context.data["handleEnd"] first_frame = node['first'].value() last_frame = node['last'].value() # colorspace colorspace = node["colorspace"].value() if "default" in colorspace: colorspace = colorspace.replace("default (", "").replace(")", "") # # Easier way to sequence - Not tested # isSequence = True # if first_frame == last_frame: # isSequence = False isSequence = False if len(items) > 1: sequence = items[-2] hash_regex = re.compile(r'([#*])') seq_regex = re.compile(r'[%0-9*d]') hash_match = re.match(hash_regex, sequence) seq_match = re.match(seq_regex, sequence) if hash_match or seq_match: isSequence = True # get source path path = nuke.filename(node) source_dir = os.path.dirname(path) self.log.debug('source dir: {}'.format(source_dir)) if isSequence: source_files = [f for f in os.listdir(source_dir) if ext in f if items[0] in f] else: source_files = file_name # Include start and end render frame in label name = node.name() label = "{0} ({1}-{2})".format( name, int(first_frame), int(last_frame) ) self.log.debug("collected_frames: {}".format(label)) if "representations" not in instance.data: instance.data["representations"] = [] representation = { 'name': ext, 'ext': ext, 'files': source_files, "stagingDir": source_dir, "frameStart": "%0{}d".format( len(str(last_frame))) % first_frame } instance.data["representations"].append(representation) transfer = node["publish"] if "publish" in node.knobs() else False instance.data['transfer'] = transfer # Add version data to instance version_data = { "handleStart": handle_start, "handleEnd": handle_end, "frameStart": first_frame + handle_start, "frameEnd": last_frame - handle_end, "colorspace": colorspace, "families": [instance.data["family"]], "subset": instance.data["subset"], "fps": instance.context.data["fps"] } instance.data.update({ "versionData": version_data, "path": path, "stagingDir": source_dir, "ext": ext, "label": label, "frameStart": first_frame, "frameEnd": last_frame, "colorspace": colorspace, "handleStart": handle_start, "handleEnd": handle_end, "step": 1, "fps": int(nuke.root()['fps'].value()) }) self.log.debug("instance.data: {}".format(instance.data)) ================================================ FILE: openpype/hosts/nuke/plugins/publish/collect_slate_node.py ================================================ import pyblish.api import nuke class CollectSlate(pyblish.api.InstancePlugin): """Check if SLATE node is in scene and connected to rendering tree""" order = pyblish.api.CollectorOrder + 0.002 label = "Collect Slate Node" hosts = ["nuke"] families = ["render"] def process(self, instance): node = instance.data["transientData"]["node"] slate = next( ( n_ for n_ in nuke.allNodes() if "slate" in n_.name().lower() if not n_["disable"].getValue() and "publish_instance" not in n_.knobs() # Exclude instance nodes. ), None ) if slate: # check if slate node is connected to write node tree slate_check = 0 slate_node = None while slate_check == 0: try: node = node.dependencies()[0] if slate.name() in node.name(): slate_node = node slate_check = 1 except IndexError: break if slate_node: instance.data["slateNode"] = slate_node instance.data["slate"] = True instance.data["families"].append("slate") self.log.debug( "Slate node is in node graph: `{}`".format(slate.name())) self.log.debug( "__ instance.data: `{}`".format(instance.data)) ================================================ FILE: openpype/hosts/nuke/plugins/publish/collect_workfile.py ================================================ import os import nuke import pyblish.api class CollectWorkfile(pyblish.api.InstancePlugin): """Collect current script for publish.""" order = pyblish.api.CollectorOrder label = "Collect Workfile" hosts = ['nuke'] families = ["workfile"] def process(self, instance): # sourcery skip: avoid-builtin-shadow script_data = instance.context.data["scriptData"] current_file = os.path.normpath(nuke.root().name()) # creating instances per write node staging_dir = os.path.dirname(current_file) base_name = os.path.basename(current_file) # creating representation representation = { 'name': 'nk', 'ext': 'nk', 'files': base_name, "stagingDir": staging_dir, } # creating instance data instance.data.update({ "name": base_name, "representations": [representation] }) # adding basic script data instance.data.update(script_data) self.log.debug( "Collected current script version: {}".format(current_file) ) ================================================ FILE: openpype/hosts/nuke/plugins/publish/collect_writes.py ================================================ import os import nuke import pyblish.api from openpype.hosts.nuke import api as napi from openpype.pipeline import publish class CollectNukeWrites(pyblish.api.InstancePlugin, publish.ColormanagedPyblishPluginMixin): """Collect all write nodes.""" order = pyblish.api.CollectorOrder + 0.0021 label = "Collect Writes" hosts = ["nuke", "nukeassist"] families = ["render", "prerender", "image"] # cache _write_nodes = {} _frame_ranges = {} def process(self, instance): group_node = instance.data["transientData"]["node"] render_target = instance.data["render_target"] write_node = self._write_node_helper(instance) if write_node is None: self.log.warning( "Created node '{}' is missing write node!".format( group_node.name() ) ) return # get colorspace and add to version data colorspace = napi.get_colorspace_from_node(write_node) if render_target == "frames": self._set_existing_files_data(instance, colorspace) elif render_target == "frames_farm": collected_frames = self._set_existing_files_data( instance, colorspace) self._set_expected_files(instance, collected_frames) self._add_farm_instance_data(instance) elif render_target == "farm": self._add_farm_instance_data(instance) # set additional instance data self._set_additional_instance_data(instance, render_target, colorspace) def _set_existing_files_data(self, instance, colorspace): """Set existing files data to instance data. Args: instance (pyblish.api.Instance): pyblish instance colorspace (str): colorspace Returns: list: collected frames """ collected_frames = self._get_collected_frames(instance) representation = self._get_existing_frames_representation( instance, collected_frames ) # inject colorspace data self.set_representation_colorspace( representation, instance.context, colorspace=colorspace ) instance.data["representations"].append(representation) return collected_frames def _set_expected_files(self, instance, collected_frames): """Set expected files to instance data. Args: instance (pyblish.api.Instance): pyblish instance collected_frames (list): collected frames """ write_node = self._write_node_helper(instance) write_file_path = nuke.filename(write_node) output_dir = os.path.dirname(write_file_path) instance.data["expectedFiles"] = [ os.path.join(output_dir, source_file) for source_file in collected_frames ] def _get_frame_range_data(self, instance): """Get frame range data from instance. Args: instance (pyblish.api.Instance): pyblish instance Returns: tuple: first_frame, last_frame """ instance_name = instance.data["name"] if self._frame_ranges.get(instance_name): # return cashed write node return self._frame_ranges[instance_name] write_node = self._write_node_helper(instance) # Get frame range from workfile first_frame = int(nuke.root()["first_frame"].getValue()) last_frame = int(nuke.root()["last_frame"].getValue()) # Get frame range from write node if activated if write_node["use_limit"].getValue(): first_frame = int(write_node["first"].getValue()) last_frame = int(write_node["last"].getValue()) # add to cache self._frame_ranges[instance_name] = (first_frame, last_frame) return first_frame, last_frame def _set_additional_instance_data( self, instance, render_target, colorspace ): """Set additional instance data. Args: instance (pyblish.api.Instance): pyblish instance render_target (str): render target colorspace (str): colorspace """ family = instance.data["family"] # add targeted family to families instance.data["families"].append( "{}.{}".format(family, render_target) ) self.log.debug("Appending render target to families: {}.{}".format( family, render_target) ) write_node = self._write_node_helper(instance) # Determine defined file type ext = write_node["file_type"].value() # get frame range data handle_start = instance.context.data["handleStart"] handle_end = instance.context.data["handleEnd"] first_frame, last_frame = self._get_frame_range_data(instance) # get output paths write_file_path = nuke.filename(write_node) output_dir = os.path.dirname(write_file_path) # TODO: remove this when we have proper colorspace support version_data = { "colorspace": colorspace } instance.data.update({ "versionData": version_data, "path": write_file_path, "outputDir": output_dir, "ext": ext, "colorspace": colorspace }) if family == "render": instance.data.update({ "handleStart": handle_start, "handleEnd": handle_end, "frameStart": first_frame + handle_start, "frameEnd": last_frame - handle_end, "frameStartHandle": first_frame, "frameEndHandle": last_frame, }) else: instance.data.update({ "handleStart": 0, "handleEnd": 0, "frameStart": first_frame, "frameEnd": last_frame, "frameStartHandle": first_frame, "frameEndHandle": last_frame, }) # TODO temporarily set stagingDir as persistent for backward # compatibility. This is mainly focused on `renders`folders which # were previously not cleaned up (and could be used in read notes) # this logic should be removed and replaced with custom staging dir instance.data["stagingDir_persistent"] = True def _write_node_helper(self, instance): """Helper function to get write node from instance. Also sets instance transient data with child nodes. Args: instance (pyblish.api.Instance): pyblish instance Returns: nuke.Node: write node """ instance_name = instance.data["name"] if self._write_nodes.get(instance_name): # return cashed write node return self._write_nodes[instance_name] # get all child nodes from group node child_nodes = napi.get_instance_group_node_childs(instance) # set child nodes to instance transient data instance.data["transientData"]["childNodes"] = child_nodes write_node = None for node_ in child_nodes: if node_.Class() == "Write": write_node = node_ if write_node: # for slate frame extraction instance.data["transientData"]["writeNode"] = write_node # add to cache self._write_nodes[instance_name] = write_node return self._write_nodes[instance_name] def _get_existing_frames_representation( self, instance, collected_frames ): """Get existing frames representation. Args: instance (pyblish.api.Instance): pyblish instance collected_frames (list): collected frames Returns: dict: representation """ first_frame, last_frame = self._get_frame_range_data(instance) write_node = self._write_node_helper(instance) write_file_path = nuke.filename(write_node) output_dir = os.path.dirname(write_file_path) # Determine defined file type ext = write_node["file_type"].value() representation = { "name": ext, "ext": ext, "stagingDir": output_dir, "tags": [] } # set slate frame collected_frames = self._add_slate_frame_to_collected_frames( instance, collected_frames, first_frame, last_frame ) if len(collected_frames) == 1: representation['files'] = collected_frames.pop() else: representation['files'] = collected_frames return representation def _get_frame_start_str(self, first_frame, last_frame): """Get frame start string. Args: first_frame (int): first frame last_frame (int): last frame Returns: str: frame start string """ # convert first frame to string with padding return ( "{{:0{}d}}".format(len(str(last_frame))) ).format(first_frame) def _add_slate_frame_to_collected_frames( self, instance, collected_frames, first_frame, last_frame ): """Add slate frame to collected frames. Args: instance (pyblish.api.Instance): pyblish instance collected_frames (list): collected frames first_frame (int): first frame last_frame (int): last frame Returns: list: collected frames """ frame_start_str = self._get_frame_start_str(first_frame, last_frame) frame_length = int(last_frame - first_frame + 1) # this will only run if slate frame is not already # rendered from previews publishes if ( "slate" in instance.data["families"] and frame_length == len(collected_frames) ): frame_slate_str = self._get_frame_start_str( first_frame - 1, last_frame ) slate_frame = collected_frames[0].replace( frame_start_str, frame_slate_str) collected_frames.insert(0, slate_frame) return collected_frames def _add_farm_instance_data(self, instance): """Add farm publishing related instance data. Args: instance (pyblish.api.Instance): pyblish instance """ # make sure rendered sequence on farm will # be used for extract review if not instance.data.get("review"): instance.data["useSequenceForReview"] = False # Farm rendering instance.data.update({ "transfer": False, "farm": True # to skip integrate }) self.log.info("Farm rendering ON ...") def _get_collected_frames(self, instance): """Get collected frames. Args: instance (pyblish.api.Instance): pyblish instance Returns: list: collected frames """ first_frame, last_frame = self._get_frame_range_data(instance) write_node = self._write_node_helper(instance) write_file_path = nuke.filename(write_node) output_dir = os.path.dirname(write_file_path) # get file path knob node_file_knob = write_node["file"] # list file paths based on input frames expected_paths = list(sorted({ node_file_knob.evaluate(frame) for frame in range(first_frame, last_frame + 1) })) # convert only to base names expected_filenames = { os.path.basename(filepath) for filepath in expected_paths } # make sure files are existing at folder collected_frames = [ filename for filename in os.listdir(output_dir) if filename in expected_filenames ] return collected_frames ================================================ FILE: openpype/hosts/nuke/plugins/publish/extract_backdrop.py ================================================ import os import nuke import pyblish.api from openpype.pipeline import publish from openpype.hosts.nuke.api.lib import ( maintained_selection, reset_selection, select_nodes ) class ExtractBackdropNode(publish.Extractor): """Extracting content of backdrop nodes Will create nuke script only with containing nodes. Also it will solve Input and Output nodes. """ order = pyblish.api.ExtractorOrder label = "Extract Backdrop" hosts = ["nuke"] families = ["nukenodes"] def process(self, instance): tmp_nodes = [] child_nodes = instance.data["transientData"]["childNodes"] # all connections outside of backdrop connections_in = instance.data["transientData"]["nodeConnectionsIn"] connections_out = instance.data["transientData"]["nodeConnectionsOut"] self.log.debug("_ connections_in: `{}`".format(connections_in)) self.log.debug("_ connections_out: `{}`".format(connections_out)) # Define extract output file path stagingdir = self.staging_dir(instance) filename = "{0}.nk".format(instance.name) path = os.path.join(stagingdir, filename) # maintain selection with maintained_selection(): # create input child_nodes and name them as passing node (*_INP) for n, inputs in connections_in.items(): for i, input in inputs: inpn = nuke.createNode("Input") inpn["name"].setValue("{}_{}_INP".format(n.name(), i)) n.setInput(i, inpn) inpn.setXYpos(input.xpos(), input.ypos()) child_nodes.append(inpn) tmp_nodes.append(inpn) reset_selection() # connect output node for n, output in connections_out.items(): opn = nuke.createNode("Output") output.setInput( next((i for i, d in enumerate(output.dependencies()) if d.name() in n.name()), 0), opn) opn.setInput(0, n) opn.autoplace() child_nodes.append(opn) tmp_nodes.append(opn) reset_selection() # select child_nodes to copy reset_selection() select_nodes(child_nodes) # create tmp nk file # save file to the path nuke.nodeCopy(path) # Clean up for tn in tmp_nodes: nuke.delete(tn) # restore original connections # reconnect input node for n, inputs in connections_in.items(): for i, input in inputs: n.setInput(i, input) # reconnect output node for n, output in connections_out.items(): output.setInput( next((i for i, d in enumerate(output.dependencies()) if d.name() in n.name()), 0), n) if "representations" not in instance.data: instance.data["representations"] = [] # create representation representation = { 'name': 'nk', 'ext': 'nk', 'files': filename, "stagingDir": stagingdir } instance.data["representations"].append(representation) self.log.debug("Extracted instance '{}' to: {}".format( instance.name, path)) ================================================ FILE: openpype/hosts/nuke/plugins/publish/extract_camera.py ================================================ import os import math from pprint import pformat import nuke import pyblish.api from openpype.pipeline import publish from openpype.hosts.nuke.api.lib import maintained_selection class ExtractCamera(publish.Extractor): """ 3D camera extractor """ label = 'Extract Camera' order = pyblish.api.ExtractorOrder families = ["camera"] hosts = ["nuke"] # presets write_geo_knobs = [ ("file_type", "abc"), ("storageFormat", "Ogawa"), ("writeGeometries", False), ("writePointClouds", False), ("writeAxes", False) ] def process(self, instance): camera_node = instance.data["transientData"]["node"] handle_start = instance.context.data["handleStart"] handle_end = instance.context.data["handleEnd"] first_frame = int(nuke.root()["first_frame"].getValue()) last_frame = int(nuke.root()["last_frame"].getValue()) step = 1 output_range = str(nuke.FrameRange(first_frame, last_frame, step)) rm_nodes = [] self.log.debug("Creating additional nodes for 3D Camera Extractor") subset = instance.data["subset"] staging_dir = self.staging_dir(instance) # get extension form preset extension = next((k[1] for k in self.write_geo_knobs if k[0] == "file_type"), None) if not extension: raise RuntimeError( "Bad config for extension in presets. " "Talk to your supervisor or pipeline admin") # create file name and path filename = subset + ".{}".format(extension) file_path = os.path.join(staging_dir, filename).replace("\\", "/") with maintained_selection(): # bake camera with axeses onto word coordinate XYZ rm_n = bakeCameraWithAxeses( camera_node, output_range) rm_nodes.append(rm_n) # create scene node rm_n = nuke.createNode("Scene") rm_nodes.append(rm_n) # create write geo node wg_n = nuke.createNode("WriteGeo") wg_n["file"].setValue(file_path) # add path to write to for k, v in self.write_geo_knobs: wg_n[k].setValue(v) rm_nodes.append(wg_n) # write out camera nuke.execute( wg_n, int(first_frame), int(last_frame) ) # erase additional nodes for n in rm_nodes: nuke.delete(n) # create representation data if "representations" not in instance.data: instance.data["representations"] = [] representation = { 'name': extension, 'ext': extension, 'files': filename, "stagingDir": staging_dir, "frameStart": first_frame, "frameEnd": last_frame } instance.data["representations"].append(representation) instance.data.update({ "path": file_path, "outputDir": staging_dir, "ext": extension, "handleStart": handle_start, "handleEnd": handle_end, "frameStart": first_frame + handle_start, "frameEnd": last_frame - handle_end, "frameStartHandle": first_frame, "frameEndHandle": last_frame, }) self.log.debug("Extracted instance '{0}' to: {1}".format( instance.name, file_path)) def bakeCameraWithAxeses(camera_node, output_range): """ Baking all perent hierarchy of axeses into camera with transposition onto word XYZ coordinance """ bakeFocal = False bakeHaperture = False bakeVaperture = False camera_matrix = camera_node['world_matrix'] new_cam_n = nuke.createNode("Camera2") new_cam_n.setInput(0, None) new_cam_n['rotate'].setAnimated() new_cam_n['translate'].setAnimated() old_focal = camera_node['focal'] if old_focal.isAnimated() and not (old_focal.animation(0).constant()): new_cam_n['focal'].setAnimated() bakeFocal = True else: new_cam_n['focal'].setValue(old_focal.value()) old_haperture = camera_node['haperture'] if old_haperture.isAnimated() and not ( old_haperture.animation(0).constant()): new_cam_n['haperture'].setAnimated() bakeHaperture = True else: new_cam_n['haperture'].setValue(old_haperture.value()) old_vaperture = camera_node['vaperture'] if old_vaperture.isAnimated() and not ( old_vaperture.animation(0).constant()): new_cam_n['vaperture'].setAnimated() bakeVaperture = True else: new_cam_n['vaperture'].setValue(old_vaperture.value()) new_cam_n['win_translate'].setValue(camera_node['win_translate'].value()) new_cam_n['win_scale'].setValue(camera_node['win_scale'].value()) for x in nuke.FrameRange(output_range): math_matrix = nuke.math.Matrix4() for y in range(camera_matrix.height()): for z in range(camera_matrix.width()): matrix_pointer = z + (y * camera_matrix.width()) math_matrix[matrix_pointer] = camera_matrix.getValueAt( x, (y + (z * camera_matrix.width()))) rot_matrix = nuke.math.Matrix4(math_matrix) rot_matrix.rotationOnly() rot = rot_matrix.rotationsZXY() new_cam_n['rotate'].setValueAt(math.degrees(rot[0]), x, 0) new_cam_n['rotate'].setValueAt(math.degrees(rot[1]), x, 1) new_cam_n['rotate'].setValueAt(math.degrees(rot[2]), x, 2) new_cam_n['translate'].setValueAt( camera_matrix.getValueAt(x, 3), x, 0) new_cam_n['translate'].setValueAt( camera_matrix.getValueAt(x, 7), x, 1) new_cam_n['translate'].setValueAt( camera_matrix.getValueAt(x, 11), x, 2) if bakeFocal: new_cam_n['focal'].setValueAt(old_focal.getValueAt(x), x) if bakeHaperture: new_cam_n['haperture'].setValueAt(old_haperture.getValueAt(x), x) if bakeVaperture: new_cam_n['vaperture'].setValueAt(old_vaperture.getValueAt(x), x) return new_cam_n ================================================ FILE: openpype/hosts/nuke/plugins/publish/extract_gizmo.py ================================================ import os import nuke import pyblish.api from openpype.pipeline import publish from openpype.hosts.nuke.api import utils as pnutils from openpype.hosts.nuke.api.lib import ( maintained_selection, reset_selection, select_nodes ) class ExtractGizmo(publish.Extractor): """Extracting Gizmo (Group) node Will create nuke script only with the Gizmo node. """ order = pyblish.api.ExtractorOrder label = "Extract Gizmo (group)" hosts = ["nuke"] families = ["gizmo"] def process(self, instance): tmp_nodes = [] orig_grpn = instance.data["transientData"]["node"] # Define extract output file path stagingdir = self.staging_dir(instance) filename = "{0}.nk".format(instance.name) path = os.path.join(stagingdir, filename) # maintain selection with maintained_selection(): orig_grpn_name = orig_grpn.name() tmp_grpn_name = orig_grpn_name + "_tmp" # select original group node select_nodes([orig_grpn]) # copy to clipboard nuke.nodeCopy("%clipboard%") # reset selection to none reset_selection() # paste clipboard nuke.nodePaste("%clipboard%") # assign pasted node copy_grpn = nuke.selectedNode() copy_grpn.setXYpos((orig_grpn.xpos() + 120), orig_grpn.ypos()) # convert gizmos to groups pnutils.bake_gizmos_recursively(copy_grpn) # add to temporary nodes tmp_nodes.append(copy_grpn) # swap names orig_grpn.setName(tmp_grpn_name) copy_grpn.setName(orig_grpn_name) # create tmp nk file # save file to the path nuke.nodeCopy(path) # Clean up for tn in tmp_nodes: nuke.delete(tn) # rename back to original orig_grpn.setName(orig_grpn_name) if "representations" not in instance.data: instance.data["representations"] = [] # create representation representation = { 'name': 'gizmo', 'ext': 'nk', 'files': filename, "stagingDir": stagingdir } instance.data["representations"].append(representation) self.log.debug("Extracted instance '{}' to: {}".format( instance.name, path)) ================================================ FILE: openpype/hosts/nuke/plugins/publish/extract_model.py ================================================ import os from pprint import pformat import nuke import pyblish.api from openpype.pipeline import publish from openpype.hosts.nuke.api.lib import ( maintained_selection, select_nodes ) class ExtractModel(publish.Extractor): """ 3D model extractor """ label = 'Extract Model' order = pyblish.api.ExtractorOrder families = ["model"] hosts = ["nuke"] # presets write_geo_knobs = [ ("file_type", "abc"), ("storageFormat", "Ogawa"), ("writeGeometries", True), ("writePointClouds", False), ("writeAxes", False) ] def process(self, instance): handle_start = instance.context.data["handleStart"] handle_end = instance.context.data["handleEnd"] first_frame = int(nuke.root()["first_frame"].getValue()) last_frame = int(nuke.root()["last_frame"].getValue()) self.log.debug("instance.data: `{}`".format( pformat(instance.data))) rm_nodes = [] model_node = instance.data["transientData"]["node"] self.log.debug("Creating additional nodes for Extract Model") subset = instance.data["subset"] staging_dir = self.staging_dir(instance) extension = next((k[1] for k in self.write_geo_knobs if k[0] == "file_type"), None) if not extension: raise RuntimeError( "Bad config for extension in presets. " "Talk to your supervisor or pipeline admin") # create file name and path filename = subset + ".{}".format(extension) file_path = os.path.join(staging_dir, filename).replace("\\", "/") with maintained_selection(): # select model node select_nodes([model_node]) # create write geo node wg_n = nuke.createNode("WriteGeo") wg_n["file"].setValue(file_path) # add path to write to for k, v in self.write_geo_knobs: wg_n[k].setValue(v) rm_nodes.append(wg_n) # write out model nuke.execute( wg_n, int(first_frame), int(last_frame) ) # erase additional nodes for n in rm_nodes: nuke.delete(n) self.log.debug("Filepath: {}".format(file_path)) # create representation data if "representations" not in instance.data: instance.data["representations"] = [] representation = { 'name': extension, 'ext': extension, 'files': filename, "stagingDir": staging_dir, "frameStart": first_frame, "frameEnd": last_frame } instance.data["representations"].append(representation) instance.data.update({ "path": file_path, "outputDir": staging_dir, "ext": extension, "handleStart": handle_start, "handleEnd": handle_end, "frameStart": first_frame + handle_start, "frameEnd": last_frame - handle_end, "frameStartHandle": first_frame, "frameEndHandle": last_frame, }) self.log.debug("Extracted instance '{0}' to: {1}".format( instance.name, file_path)) ================================================ FILE: openpype/hosts/nuke/plugins/publish/extract_ouput_node.py ================================================ import nuke import pyblish.api from openpype.hosts.nuke.api.lib import maintained_selection class CreateOutputNode(pyblish.api.ContextPlugin): """Adding output node for each output write node So when latly user will want to Load .nk as LifeGroup or Precomp Nuke will not complain about missing Output node """ label = 'Output Node Create' order = pyblish.api.ExtractorOrder + 0.4 families = ["workfile"] hosts = ['nuke'] def process(self, context): # capture selection state with maintained_selection(): active_node = [ inst.data.get("transientData", {}).get("node") for inst in context if inst.data.get("transientData", {}).get("node") if inst.data.get( "transientData", {}).get("node").Class() != "Root" ] if active_node: active_node = active_node.pop() self.log.debug("Active node: {}".format(active_node)) active_node['selected'].setValue(True) # select only instance render node output_node = nuke.createNode("Output") # deselect all and select the original selection output_node['selected'].setValue(False) # save script nuke.scriptSave() # add node to instance node list context.data["outputNode"] = output_node ================================================ FILE: openpype/hosts/nuke/plugins/publish/extract_output_directory.py ================================================ import os import pyblish.api class ExtractOutputDirectory(pyblish.api.InstancePlugin): """Extracts the output path for any collection or single output_path.""" order = pyblish.api.ExtractorOrder - 0.05 label = "Output Directory" optional = True # targets = ["process"] def process(self, instance): path = None if "output_path" in instance.data.keys(): path = instance.data["path"] if not path: return if not os.path.exists(os.path.dirname(path)): os.makedirs(os.path.dirname(path)) ================================================ FILE: openpype/hosts/nuke/plugins/publish/extract_render_local.py ================================================ import os import shutil import pyblish.api import clique import nuke from openpype.hosts.nuke import api as napi from openpype.pipeline import publish from openpype.lib import collect_frames class NukeRenderLocal(publish.Extractor, publish.ColormanagedPyblishPluginMixin): """Render the current Nuke composition locally. Extract the result of savers by starting a comp render This will run the local render of Fusion. Allows to use last published frames and overwrite only specific ones (set in instance.data.get("frames_to_fix")) """ order = pyblish.api.ExtractorOrder label = "Render Local" hosts = ["nuke"] families = ["render.local", "prerender.local", "image.local"] def process(self, instance): child_nodes = ( instance.data.get("transientData", {}).get("childNodes") or instance ) node = None for x in child_nodes: if x.Class() == "Write": node = x self.log.debug("instance collected: {}".format(instance.data)) node_subset_name = instance.data.get("name", None) first_frame = instance.data.get("frameStartHandle", None) last_frame = instance.data.get("frameEndHandle", None) filenames = [] node_file = node["file"] # Collect expected filepaths for each frame # - for cases that output is still image is first created set of # paths which is then sorted and converted to list expected_paths = list(sorted({ node_file.evaluate(frame) for frame in range(first_frame, last_frame + 1) })) # Extract only filenames for representation filenames.extend([ os.path.basename(filepath) for filepath in expected_paths ]) # Ensure output directory exists. out_dir = os.path.dirname(expected_paths[0]) if not os.path.exists(out_dir): os.makedirs(out_dir) frames_to_render = [(first_frame, last_frame)] frames_to_fix = instance.data.get("frames_to_fix") if instance.data.get("last_version_published_files") and frames_to_fix: frames_to_render = self._get_frames_to_render(frames_to_fix) anatomy = instance.context.data["anatomy"] self._copy_last_published(anatomy, instance, out_dir, filenames) for render_first_frame, render_last_frame in frames_to_render: self.log.info("Starting render") self.log.info("Start frame: {}".format(render_first_frame)) self.log.info("End frame: {}".format(render_last_frame)) # Render frames nuke.execute( str(node_subset_name), int(render_first_frame), int(render_last_frame) ) ext = node["file_type"].value() colorspace = napi.get_colorspace_from_node(node) if "representations" not in instance.data: instance.data["representations"] = [] if len(filenames) == 1: repre = { 'name': ext, 'ext': ext, 'files': filenames[0], "stagingDir": out_dir } else: repre = { 'name': ext, 'ext': ext, 'frameStart': ( "{{:0>{}}}" .format(len(str(last_frame))) .format(first_frame) ), 'files': filenames, "stagingDir": out_dir } # inject colorspace data self.set_representation_colorspace( repre, instance.context, colorspace=colorspace ) instance.data["representations"].append(repre) self.log.debug("Extracted instance '{0}' to: {1}".format( instance.name, out_dir )) families = instance.data["families"] # redefinition of families if "render.local" in families: instance.data['family'] = 'render' families.remove('render.local') families.insert(0, "render2d") instance.data["anatomyData"]["family"] = "render" elif "prerender.local" in families: instance.data['family'] = 'prerender' families.remove('prerender.local') families.insert(0, "prerender") instance.data["anatomyData"]["family"] = "prerender" elif "image.local" in families: instance.data['family'] = 'image' families.remove('image.local') instance.data["anatomyData"]["family"] = "image" instance.data["families"] = families collections, remainder = clique.assemble(filenames) self.log.debug('collections: {}'.format(str(collections))) if collections: collection = collections[0] instance.data['collection'] = collection self.log.info('Finished render') self.log.debug("_ instance.data: {}".format(instance.data)) def _copy_last_published(self, anatomy, instance, out_dir, expected_filenames): """Copies last published files to temporary out_dir. These are base of files which will be extended/fixed for specific frames. Renames published file to expected file name based on frame, eg. test_project_test_asset_subset_v005.1001.exr > new_render.1001.exr """ last_published = instance.data["last_version_published_files"] last_published_and_frames = collect_frames(last_published) expected_and_frames = collect_frames(expected_filenames) frames_and_expected = {v: k for k, v in expected_and_frames.items()} for file_path, frame in last_published_and_frames.items(): file_path = anatomy.fill_root(file_path) if not os.path.exists(file_path): continue target_file_name = frames_and_expected.get(frame) if not target_file_name: continue out_path = os.path.join(out_dir, target_file_name) self.log.debug("Copying '{}' -> '{}'".format(file_path, out_path)) shutil.copy(file_path, out_path) # TODO shouldn't this be uncommented # instance.context.data["cleanupFullPaths"].append(out_path) def _get_frames_to_render(self, frames_to_fix): """Return list of frame range tuples to render Args: frames_to_fix (str): specific or range of frames to be rerendered (1005, 1009-1010) Returns: (list): [(1005, 1005), (1009-1010)] """ frames_to_render = [] for frame_range in frames_to_fix.split(","): if frame_range.isdigit(): render_first_frame = frame_range render_last_frame = frame_range elif '-' in frame_range: frames = frame_range.split('-') render_first_frame = int(frames[0]) render_last_frame = int(frames[1]) else: raise ValueError("Wrong format of frames to fix {}" .format(frames_to_fix)) frames_to_render.append((render_first_frame, render_last_frame)) return frames_to_render ================================================ FILE: openpype/hosts/nuke/plugins/publish/extract_review_data.py ================================================ import os from pprint import pformat import pyblish.api from openpype.pipeline import publish class ExtractReviewData(publish.Extractor): """Extracts review tag into available representation """ order = pyblish.api.ExtractorOrder + 0.01 # order = pyblish.api.CollectorOrder + 0.499 label = "Extract Review Data" families = ["review"] hosts = ["nuke"] def process(self, instance): fpath = instance.data["path"] ext = os.path.splitext(fpath)[-1][1:] representations = instance.data.get("representations", []) # review can be removed since `ProcessSubmittedJobOnFarm` will create # reviewable representation if needed if ( instance.data.get("farm") and "review" in instance.data["families"] ): instance.data["families"].remove("review") # iterate representations and add `review` tag for repre in representations: if ext != repre["ext"]: continue if not repre.get("tags"): repre["tags"] = [] if "review" not in repre["tags"]: repre["tags"].append("review") self.log.debug("Matching representation: {}".format( pformat(repre) )) instance.data["representations"] = representations ================================================ FILE: openpype/hosts/nuke/plugins/publish/extract_review_data_lut.py ================================================ import os import pyblish.api from openpype.pipeline import publish from openpype.hosts.nuke.api import plugin from openpype.hosts.nuke.api.lib import maintained_selection class ExtractReviewDataLut(publish.Extractor): """Extracts movie and thumbnail with baked in luts must be run after extract_render_local.py """ order = pyblish.api.ExtractorOrder + 0.005 label = "Extract Review Data Lut" families = ["review"] hosts = ["nuke"] def process(self, instance): self.log.debug("Creating staging dir...") if "representations" in instance.data: staging_dir = instance.data[ "representations"][0]["stagingDir"].replace("\\", "/") instance.data["stagingDir"] = staging_dir instance.data["representations"][0]["tags"] = ["review"] else: instance.data["representations"] = [] # get output path render_path = instance.data['path'] staging_dir = os.path.normpath(os.path.dirname(render_path)) instance.data["stagingDir"] = staging_dir self.log.debug( "StagingDir `{0}`...".format(instance.data["stagingDir"])) # generate data with maintained_selection(): exporter = plugin.ExporterReviewLut( self, instance ) data = exporter.generate_lut() # assign to representations instance.data["lutPath"] = os.path.join( exporter.stagingDir, exporter.file).replace("\\", "/") instance.data["representations"] += data["representations"] # review can be removed since `ProcessSubmittedJobOnFarm` will create # reviewable representation if needed if ( instance.data.get("farm") and "review" in instance.data["families"] ): instance.data["families"].remove("review") self.log.debug( "_ lutPath: {}".format(instance.data["lutPath"])) self.log.debug( "_ representations: {}".format(instance.data["representations"])) ================================================ FILE: openpype/hosts/nuke/plugins/publish/extract_review_intermediates.py ================================================ import os import re from pprint import pformat import pyblish.api from openpype.pipeline import publish from openpype.hosts.nuke.api import plugin from openpype.hosts.nuke.api.lib import maintained_selection class ExtractReviewIntermediates(publish.Extractor): """Extracting intermediate videos or sequences with thumbnail for transcoding. must be run after extract_render_local.py """ order = pyblish.api.ExtractorOrder + 0.01 label = "Extract Review Intermediates" families = ["review"] hosts = ["nuke"] # presets viewer_lut_raw = None outputs = {} @classmethod def apply_settings(cls, project_settings): """Apply the settings from the deprecated ExtractReviewDataMov plugin for backwards compatibility """ nuke_publish = project_settings["nuke"]["publish"] deprecated_setting = nuke_publish["ExtractReviewDataMov"] current_setting = nuke_publish.get("ExtractReviewIntermediates") if not deprecated_setting["enabled"] and ( not current_setting["enabled"] ): cls.enabled = False if deprecated_setting["enabled"]: # Use deprecated settings if they are still enabled cls.viewer_lut_raw = deprecated_setting["viewer_lut_raw"] cls.outputs = deprecated_setting["outputs"] elif current_setting is None: pass elif current_setting["enabled"]: cls.viewer_lut_raw = current_setting["viewer_lut_raw"] cls.outputs = current_setting["outputs"] def process(self, instance): families = set(instance.data["families"]) # add main family to make sure all families are compared families.add(instance.data["family"]) task_type = instance.context.data["taskType"] subset = instance.data["subset"] self.log.debug("Creating staging dir...") if "representations" not in instance.data: instance.data["representations"] = [] staging_dir = os.path.normpath( os.path.dirname(instance.data["path"])) instance.data["stagingDir"] = staging_dir self.log.debug( "StagingDir `{0}`...".format(instance.data["stagingDir"])) self.log.debug("Outputs: {}".format(self.outputs)) # generate data with maintained_selection(): generated_repres = [] for o_name, o_data in self.outputs.items(): self.log.debug( "o_name: {}, o_data: {}".format(o_name, pformat(o_data))) f_families = o_data["filter"]["families"] f_task_types = o_data["filter"]["task_types"] f_subsets = o_data["filter"]["subsets"] self.log.debug( "f_families `{}` > families: {}".format( f_families, families)) self.log.debug( "f_task_types `{}` > task_type: {}".format( f_task_types, task_type)) self.log.debug( "f_subsets `{}` > subset: {}".format( f_subsets, subset)) # test if family found in context # using intersection to make sure all defined # families are present in combination if f_families and not families.intersection(f_families): continue # test task types from filter if f_task_types and task_type not in f_task_types: continue # test subsets from filter if f_subsets and not any( re.search(s, subset) for s in f_subsets): continue self.log.debug( "Baking output `{}` with settings: {}".format( o_name, o_data) ) # check if settings have more then one preset # so we dont need to add outputName to representation # in case there is only one preset multiple_presets = len(self.outputs.keys()) > 1 # adding bake presets to instance data for other plugins if not instance.data.get("bakePresets"): instance.data["bakePresets"] = {} # add preset to bakePresets instance.data["bakePresets"][o_name] = o_data # create exporter instance exporter = plugin.ExporterReviewMov( self, instance, o_name, o_data["extension"], multiple_presets) if instance.data.get("farm"): if "review" in instance.data["families"]: instance.data["families"].remove("review") data = exporter.generate_mov(farm=True, **o_data) self.log.debug( "_ data: {}".format(data)) if not instance.data.get("bakingNukeScripts"): instance.data["bakingNukeScripts"] = [] instance.data["bakingNukeScripts"].append({ "bakeRenderPath": data.get("bakeRenderPath"), "bakeScriptPath": data.get("bakeScriptPath"), "bakeWriteNodeName": data.get("bakeWriteNodeName") }) else: data = exporter.generate_mov(**o_data) # add representation generated by exporter generated_repres.extend(data["representations"]) self.log.debug( "__ generated_repres: {}".format(generated_repres)) if generated_repres: # assign to representations instance.data["representations"] += generated_repres instance.data["useSequenceForReview"] = False else: instance.data["families"].remove("review") self.log.debug( "Removing `review` from families. " "Not available baking profile." ) self.log.debug(instance.data["families"]) self.log.debug( "_ representations: {}".format( instance.data["representations"])) ================================================ FILE: openpype/hosts/nuke/plugins/publish/extract_script_save.py ================================================ import nuke import pyblish.api class ExtractScriptSave(pyblish.api.Extractor): """Save current Nuke workfile script""" label = 'Script Save' order = pyblish.api.Extractor.order - 0.1 hosts = ['nuke'] def process(self, instance): self.log.debug('Saving current script') nuke.scriptSave() ================================================ FILE: openpype/hosts/nuke/plugins/publish/extract_slate_frame.py ================================================ import os from pprint import pformat import nuke import copy import pyblish.api import six from openpype.pipeline import publish from openpype.hosts.nuke.api import ( maintained_selection, duplicate_node, get_view_process_node ) class ExtractSlateFrame(publish.Extractor): """Extracts movie and thumbnail with baked in luts must be run after extract_render_local.py """ order = pyblish.api.ExtractorOrder + 0.011 label = "Extract Slate Frame" families = ["slate"] hosts = ["nuke"] # Settings values key_value_mapping = { "f_submission_note": [True, "{comment}"], "f_submitting_for": [True, "{intent[value]}"], "f_vfx_scope_of_work": [False, ""] } def process(self, instance): if "representations" not in instance.data: instance.data["representations"] = [] self._create_staging_dir(instance) with maintained_selection(): self.log.debug("instance: {}".format(instance)) self.log.debug("instance.data[families]: {}".format( instance.data["families"])) if instance.data.get("bakePresets"): for o_name, o_data in instance.data["bakePresets"].items(): self.log.debug("_ o_name: {}, o_data: {}".format( o_name, pformat(o_data))) self.render_slate( instance, o_name, o_data["bake_viewer_process"], o_data["bake_viewer_input_process"] ) else: # backward compatibility self.render_slate(instance) # also render image to sequence self._render_slate_to_sequence(instance) def _create_staging_dir(self, instance): self.log.debug("Creating staging dir...") staging_dir = os.path.normpath( os.path.dirname(instance.data["path"])) instance.data["stagingDir"] = staging_dir self.log.debug( "StagingDir `{0}`...".format(instance.data["stagingDir"])) def _check_frames_exists(self, instance): # rendering path from group write node fpath = instance.data["path"] # instance frame range with handles first = instance.data["frameStartHandle"] last = instance.data["frameEndHandle"] padding = fpath.count('#') test_path_template = fpath if padding: repl_string = "#" * padding test_path_template = fpath.replace( repl_string, "%0{}d".format(padding)) for frame in range(first, last + 1): test_file = test_path_template % frame if not os.path.exists(test_file): self.log.debug("__ test_file: `{}`".format(test_file)) return None return True def render_slate( self, instance, output_name=None, bake_viewer_process=True, bake_viewer_input_process=True ): """Slate frame renderer Args: instance (PyblishInstance): Pyblish instance with subset data output_name (str, optional): Slate variation name. Defaults to None. bake_viewer_process (bool, optional): Switch for viewer profile baking. Defaults to True. bake_viewer_input_process (bool, optional): Switch for input process node baking. Defaults to True. """ slate_node = instance.data["slateNode"] # rendering path from group write node fpath = instance.data["path"] # instance frame range with handles first_frame = instance.data["frameStartHandle"] last_frame = instance.data["frameEndHandle"] # fill slate node with comments self.add_comment_slate_node(instance, slate_node) # solve output name if any is set _output_name = output_name or "" if _output_name: _output_name = "_" + _output_name slate_first_frame = first_frame - 1 collection = instance.data.get("collection", None) if collection: # get path fname = os.path.basename(collection.format( "{head}{padding}{tail}")) fhead = collection.format("{head}") else: fname = os.path.basename(fpath) fhead = os.path.splitext(fname)[0] + "." if "#" in fhead: fhead = fhead.replace("#", "")[:-1] self.log.debug("__ first_frame: {}".format(first_frame)) self.log.debug("__ slate_first_frame: {}".format(slate_first_frame)) above_slate_node = slate_node.dependencies().pop() # fallback if files does not exists if self._check_frames_exists(instance): # Read node r_node = nuke.createNode("Read") r_node["file"].setValue(fpath) r_node["first"].setValue(first_frame) r_node["origfirst"].setValue(first_frame) r_node["last"].setValue(last_frame) r_node["origlast"].setValue(last_frame) r_node["colorspace"].setValue(instance.data["colorspace"]) previous_node = r_node temporary_nodes = [previous_node] # adding copy metadata node for correct frame metadata cm_node = nuke.createNode("CopyMetaData") cm_node.setInput(0, previous_node) cm_node.setInput(1, above_slate_node) previous_node = cm_node temporary_nodes.append(cm_node) else: previous_node = above_slate_node temporary_nodes = [] # only create colorspace baking if toggled on if bake_viewer_process: if bake_viewer_input_process: # get input process and connect it to baking ipn = get_view_process_node() if ipn is not None: ipn.setInput(0, previous_node) previous_node = ipn temporary_nodes.append(ipn) # add duplicate slate node and connect to previous duply_slate_node = duplicate_node(slate_node) duply_slate_node.setInput(0, previous_node) previous_node = duply_slate_node temporary_nodes.append(duply_slate_node) # add viewer display transformation node dag_node = nuke.createNode("OCIODisplay") dag_node.setInput(0, previous_node) previous_node = dag_node temporary_nodes.append(dag_node) else: # add duplicate slate node and connect to previous duply_slate_node = duplicate_node(slate_node) duply_slate_node.setInput(0, previous_node) previous_node = duply_slate_node temporary_nodes.append(duply_slate_node) # create write node write_node = nuke.createNode("Write") file = fhead[:-1] + _output_name + "_slate.png" path = os.path.join( instance.data["stagingDir"], file).replace("\\", "/") # add slate path to `slateFrames` instance data attr if not instance.data.get("slateFrames"): instance.data["slateFrames"] = {} instance.data["slateFrames"][output_name or "*"] = path # create write node write_node["file"].setValue(path) write_node["file_type"].setValue("png") write_node["raw"].setValue(1) write_node.setInput(0, previous_node) temporary_nodes.append(write_node) # Render frames nuke.execute( write_node.name(), int(slate_first_frame), int(slate_first_frame)) # Clean up for node in temporary_nodes: nuke.delete(node) def _render_slate_to_sequence(self, instance): # set slate frame first_frame = instance.data["frameStartHandle"] last_frame = instance.data["frameEndHandle"] slate_first_frame = first_frame - 1 # render slate as sequence frame nuke.execute( instance.data["name"], int(slate_first_frame), int(slate_first_frame) ) # Add file to representation files # - get write node write_node = instance.data["transientData"]["writeNode"] # - evaluate filepaths for first frame and slate frame first_filename = os.path.basename( write_node["file"].evaluate(first_frame)) slate_filename = os.path.basename( write_node["file"].evaluate(slate_first_frame)) # Find matching representation based on first filename matching_repre = None is_sequence = None for repre in instance.data["representations"]: files = repre["files"] if ( not isinstance(files, six.string_types) and first_filename in files ): matching_repre = repre is_sequence = True break elif files == first_filename: matching_repre = repre is_sequence = False break if not matching_repre: self.log.info( "Matching representation was not found." " Representation files were not filled with slate." ) return # Add frame to matching representation files if not is_sequence: matching_repre["files"] = [first_filename, slate_filename] elif slate_filename not in matching_repre["files"]: matching_repre["files"].insert(0, slate_filename) matching_repre["frameStart"] = ( "{{:0>{}}}" .format(len(str(last_frame))) .format(slate_first_frame) ) self.log.debug( "__ matching_repre: {}".format(pformat(matching_repre))) self.log.info("Added slate frame to representation files") def add_comment_slate_node(self, instance, node): comment = instance.data["comment"] intent = instance.context.data.get("intent") if not isinstance(intent, dict): intent = { "label": intent, "value": intent } fill_data = copy.deepcopy(instance.data["anatomyData"]) fill_data.update({ "custom": copy.deepcopy( instance.data.get("customData") or {} ), "comment": comment, "intent": intent }) for key, _values in self.key_value_mapping.items(): enabled, template = _values if not enabled: self.log.debug("Key \"{}\" is disabled".format(key)) continue try: value = template.format(**fill_data) except ValueError: self.log.warning( "Couldn't fill template \"{}\" with data: {}".format( template, fill_data ), exc_info=True ) continue except KeyError: self.log.warning( ( "Template contains unknown key." " Template \"{}\" Data: {}" ).format(template, fill_data), exc_info=True ) continue try: node[key].setValue(value) self.log.debug("Change key \"{}\" to value \"{}\"".format( key, value )) except NameError: self.log.warning(( "Failed to set value \"{0}\" on node attribute \"{0}\"" ).format(value)) ================================================ FILE: openpype/hosts/nuke/plugins/publish/help/validate_asset_context.xml ================================================ ================================================ FILE: openpype/hosts/nuke/plugins/publish/help/validate_backdrop.xml ================================================ Shot/Asset name ## Publishing to a different asset context There are publish instances present which are publishing into a different asset than your current context. Usually this is not what you want but there can be cases where you might want to publish into another asset/shot or task. If that's the case you can disable the validation on the instance to ignore it. The wrong node's name is: \`{node_name}\` ### Correct context keys and values: \`{correct_values}\` ### Wrong keys and values: \`{wrong_values}\`. ## How to repair? 1. Use \"Repair\" button. 2. Hit Reload button on the publisher. ================================================ FILE: openpype/hosts/nuke/plugins/publish/help/validate_gizmo.xml ================================================ Found multiple outputs ## Invalid output amount Backdrop is having more than one outgoing connections. ### How to repair? 1. Use button `Center node in node graph` and navigate to the backdrop. 2. Reorganize nodes the way only one outgoing connection is present. 3. Hit reload button on the publisher. ### How could this happen? More than one node, which are found above the backdrop, are linked downstream or more output connections from a node also linked downstream. Empty backdrop ## Invalid empty backdrop Backdrop is empty and no nodes are found above it. ### How to repair? 1. Use button `Center node in node graph` and navigate to the backdrop. 2. Add any node above it or delete it. 3. Hit reload button on the publisher. ================================================ FILE: openpype/hosts/nuke/plugins/publish/help/validate_knobs.xml ================================================ Found multiple outputs ## Invalid amount of Output nodes Group node `{node_name}` is having more than one Output node. ### How to repair? 1. Use button `Open Group`. 2. Remove redundant Output node. 3. Hit reload button on the publisher. ### How could this happen? Perhaps you had created exciently more than one Output node. Missing Input nodes ## Missing Input nodes Make sure there is at least one connected Input node inside the group node with name `{node_name}` ### How to repair? 1. Use button `Open Group`. 2. Add at least one Input node and connect to other nodes. 3. Hit reload button on the publisher. ================================================ FILE: openpype/hosts/nuke/plugins/publish/help/validate_output_resolution.xml ================================================ Knobs value ## Invalid node's knobs values Following node knobs needs to be repaired: {invalid_items} ### How to repair? 1. Use Repair button. 2. Hit Reload button on the publisher. ================================================ FILE: openpype/hosts/nuke/plugins/publish/help/validate_proxy_mode.xml ================================================ Output format ## Invalid format setting Either the Reformat node inside of the render group is missing or the Reformat node output format knob is not set to `root.format`. ### How to repair? 1. Use Repair button. 2. Hit Reload button on the publisher. ================================================ FILE: openpype/hosts/nuke/plugins/publish/help/validate_rendered_frames.xml ================================================ Proxy mode ## Invalid proxy mode value Nuke is set to use Proxy. This is not supported by publisher. ### How to repair? 1. Use Repair button. 2. Hit Reload button on the publisher. ================================================ FILE: openpype/hosts/nuke/plugins/publish/help/validate_script_attributes.xml ================================================ Rendered Frames ## Missing Rendered Frames Render node "{node_name}" is set to "Use existing frames", but frames are missing. ### How to repair? 1. Use Repair button. 2. Set different target. 2. Hit Reload button on the publisher. ================================================ FILE: openpype/hosts/nuke/plugins/publish/help/validate_write_nodes.xml ================================================ Script attributes ## Invalid Script attributes Following script root attributes need to be fixed: {failed_attributes} ### How to repair? 1. Use Repair. 2. Hit Reload button on the publisher. ================================================ FILE: openpype/hosts/nuke/plugins/publish/increment_script_version.py ================================================ import nuke import pyblish.api class IncrementScriptVersion(pyblish.api.ContextPlugin): """Increment current script version.""" order = pyblish.api.IntegratorOrder + 0.9 label = "Increment Script Version" optional = True families = ["workfile"] hosts = ['nuke'] def process(self, context): assert all(result["success"] for result in context.data["results"]), ( "Publishing not successful so version is not increased.") from openpype.lib import version_up path = context.data["currentFile"] nuke.scriptSaveAs(version_up(path)) self.log.info('Incrementing script version') ================================================ FILE: openpype/hosts/nuke/plugins/publish/remove_ouput_node.py ================================================ import nuke import pyblish.api class RemoveOutputNode(pyblish.api.ContextPlugin): """Removing output node for each output write node """ label = 'Output Node Remove' order = pyblish.api.IntegratorOrder + 0.4 families = ["workfile"] hosts = ['nuke'] def process(self, context): try: output_node = context.data["outputNode"] name = output_node["name"].value() self.log.info("Removing output node: '{}'".format(name)) nuke.delete(output_node) except Exception: return ================================================ FILE: openpype/hosts/nuke/plugins/publish/validate_asset_context.py ================================================ # -*- coding: utf-8 -*- """Validate if instance asset is the same as context asset.""" from __future__ import absolute_import import pyblish.api from openpype.pipeline.publish import ( RepairAction, ValidateContentsOrder, PublishXmlValidationError, OptionalPyblishPluginMixin ) from openpype.hosts.nuke.api import SelectInstanceNodeAction class ValidateCorrectAssetContext( pyblish.api.InstancePlugin, OptionalPyblishPluginMixin ): """Validator to check if instance asset context match context asset. When working in per-shot style you always publish data in context of current asset (shot). This validator checks if this is so. It is optional so it can be disabled when needed. Checking `asset` and `task` keys. """ order = ValidateContentsOrder label = "Validate asset context" hosts = ["nuke"] actions = [ RepairAction, SelectInstanceNodeAction ] optional = True @classmethod def apply_settings(cls, project_settings): """Apply deprecated settings from project settings. """ nuke_publish = project_settings["nuke"]["publish"] if "ValidateCorrectAssetName" in nuke_publish: settings = nuke_publish["ValidateCorrectAssetName"] else: settings = nuke_publish["ValidateCorrectAssetContext"] cls.enabled = settings["enabled"] cls.optional = settings["optional"] cls.active = settings["active"] def process(self, instance): if not self.is_active(instance.data): return invalid_keys = self.get_invalid(instance) if not invalid_keys: return message_values = { "node_name": instance.data["transientData"]["node"].name(), "correct_values": ", ".join([ "{} > {}".format(_key, instance.context.data[_key]) for _key in invalid_keys ]), "wrong_values": ", ".join([ "{} > {}".format(_key, instance.data.get(_key)) for _key in invalid_keys ]) } msg = ( "Instance `{node_name}` has wrong context keys:\n" "Correct: `{correct_values}` | Wrong: `{wrong_values}`").format( **message_values) self.log.debug(msg) raise PublishXmlValidationError( self, msg, formatting_data=message_values ) @classmethod def get_invalid(cls, instance): """Get invalid keys from instance data and context data.""" invalid_keys = [] testing_keys = ["asset", "task"] for _key in testing_keys: if _key not in instance.data: invalid_keys.append(_key) continue if instance.data[_key] != instance.context.data[_key]: invalid_keys.append(_key) return invalid_keys @classmethod def repair(cls, instance): """Repair instance data with context data.""" invalid_keys = cls.get_invalid(instance) create_context = instance.context.data["create_context"] instance_id = instance.data.get("instance_id") created_instance = create_context.get_instance_by_id( instance_id ) for _key in invalid_keys: created_instance[_key] = instance.context.data[_key] create_context.save_changes() ================================================ FILE: openpype/hosts/nuke/plugins/publish/validate_backdrop.py ================================================ import nuke import pyblish from openpype.hosts.nuke import api as napi from openpype.pipeline.publish import ( ValidateContentsOrder, PublishXmlValidationError, OptionalPyblishPluginMixin ) class SelectCenterInNodeGraph(pyblish.api.Action): """ Centering failed instance node in node grap """ label = "Center node in node graph" icon = "wrench" on = "failed" def process(self, context, plugin): # Get the errored instances failed = [] for result in context.data["results"]: if (result["error"] is not None and result["instance"] is not None and result["instance"] not in failed): failed.append(result["instance"]) # Apply pyblish.logic to get the instances for the plug-in instances = pyblish.api.instances_by_plugin(failed, plugin) all_xC = [] all_yC = [] # maintain selection with napi.maintained_selection(): # collect all failed nodes xpos and ypos for instance in instances: bdn = instance.data["transientData"]["node"] xC = bdn.xpos() + bdn.screenWidth() / 2 yC = bdn.ypos() + bdn.screenHeight() / 2 all_xC.append(xC) all_yC.append(yC) self.log.debug("all_xC: `{}`".format(all_xC)) self.log.debug("all_yC: `{}`".format(all_yC)) # zoom to nodes in node graph nuke.zoom(2, [min(all_xC), min(all_yC)]) class ValidateBackdrop( pyblish.api.InstancePlugin, OptionalPyblishPluginMixin ): """ Validate amount of nodes on backdrop node in case user forgotten to add nodes above the publishing backdrop node. """ order = ValidateContentsOrder optional = True families = ["nukenodes"] label = "Validate Backdrop" hosts = ["nuke"] actions = [SelectCenterInNodeGraph] def process(self, instance): if not self.is_active(instance.data): return child_nodes = instance.data["transientData"]["childNodes"] connections_out = instance.data["transientData"]["nodeConnectionsOut"] msg_multiple_outputs = ( "Only one outcoming connection from " "\"{}\" is allowed").format(instance.data["name"]) if len(connections_out.keys()) > 1: raise PublishXmlValidationError( self, msg_multiple_outputs, "multiple_outputs" ) msg_no_nodes = "No content on backdrop node: \"{}\"".format( instance.data["name"]) self.log.debug( "Amount of nodes on instance: {}".format( len(child_nodes)) ) if child_nodes == []: raise PublishXmlValidationError( self, msg_no_nodes, "no_nodes" ) ================================================ FILE: openpype/hosts/nuke/plugins/publish/validate_exposed_knobs.py ================================================ import pyblish.api from openpype.pipeline.publish import get_errored_instances_from_context from openpype.hosts.nuke.api.lib import link_knobs from openpype.pipeline.publish import ( OptionalPyblishPluginMixin, PublishValidationError ) class RepairExposedKnobs(pyblish.api.Action): label = "Repair" on = "failed" icon = "wrench" def process(self, context, plugin): instances = get_errored_instances_from_context(context) for instance in instances: child_nodes = ( instance.data.get("transientData", {}).get("childNodes") or instance ) write_group_node = instance.data["transientData"]["node"] # get write node from inside of group write_node = None for x in child_nodes: if x.Class() == "Write": write_node = x plugin_name = plugin.families_mapping[instance.data["family"]] nuke_settings = instance.context.data["project_settings"]["nuke"] create_settings = nuke_settings["create"][plugin_name] exposed_knobs = create_settings["exposed_knobs"] link_knobs(exposed_knobs, write_node, write_group_node) class ValidateExposedKnobs( OptionalPyblishPluginMixin, pyblish.api.InstancePlugin ): """ Validate write node exposed knobs. Compare exposed linked knobs to settings. """ order = pyblish.api.ValidatorOrder optional = True families = ["render", "prerender", "image"] label = "Validate Exposed Knobs" actions = [RepairExposedKnobs] hosts = ["nuke"] families_mapping = { "render": "CreateWriteRender", "prerender": "CreateWritePrerender", "image": "CreateWriteImage" } def process(self, instance): if not self.is_active(instance.data): return plugin = self.families_mapping[instance.data["family"]] group_node = instance.data["transientData"]["node"] nuke_settings = instance.context.data["project_settings"]["nuke"] create_settings = nuke_settings["create"][plugin] exposed_knobs = create_settings.get("exposed_knobs", []) unexposed_knobs = [] for knob in exposed_knobs: if knob not in group_node.knobs(): unexposed_knobs.append(knob) if unexposed_knobs: raise PublishValidationError( "Missing exposed knobs: {}".format(unexposed_knobs) ) ================================================ FILE: openpype/hosts/nuke/plugins/publish/validate_gizmo.py ================================================ import pyblish from openpype.pipeline import PublishXmlValidationError from openpype.hosts.nuke import api as napi import nuke class OpenFailedGroupNode(pyblish.api.Action): """ Centering failed instance node in node grap """ label = "Open Group" icon = "wrench" on = "failed" def process(self, context, plugin): # Get the errored instances failed = [] for result in context.data["results"]: if (result["error"] is not None and result["instance"] is not None and result["instance"] not in failed): failed.append(result["instance"]) # Apply pyblish.logic to get the instances for the plug-in instances = pyblish.api.instances_by_plugin(failed, plugin) # maintain selection with napi.maintained_selection(): # collect all failed nodes xpos and ypos for instance in instances: grpn = instance.data["transientData"]["node"] nuke.showDag(grpn) class ValidateGizmo(pyblish.api.InstancePlugin): """Validate amount of output nodes in gizmo (group) node""" order = pyblish.api.ValidatorOrder optional = True families = ["gizmo"] label = "Validate Gizmo (group)" hosts = ["nuke"] actions = [OpenFailedGroupNode] def process(self, instance): grpn = instance.data["transientData"]["node"] with grpn: connections_out = nuke.allNodes('Output') if len(connections_out) > 1: msg_multiple_outputs = ( "Only one outcoming connection from " "\"{}\" is allowed").format(instance.data["name"]) raise PublishXmlValidationError( self, msg_multiple_outputs, "multiple_outputs", {"node_name": grpn["name"].value()} ) connections_in = nuke.allNodes('Input') if len(connections_in) == 0: msg_missing_inputs = ( "At least one Input node has to be inside Group: " "\"{}\"").format(instance.data["name"]) raise PublishXmlValidationError( self, msg_missing_inputs, "no_inputs", {"node_name": grpn["name"].value()} ) ================================================ FILE: openpype/hosts/nuke/plugins/publish/validate_knobs.py ================================================ import nuke import six import pyblish.api from openpype.pipeline.publish import ( RepairContextAction, PublishXmlValidationError, ) class ValidateKnobs(pyblish.api.ContextPlugin): """Ensure knobs are consistent. Knobs to validate and their values comes from the Controlled by plugin settings that require json in following structure: "ValidateKnobs": { "enabled": true, "knobs": { "family": { "knob_name": knob_value } } } """ order = pyblish.api.ValidatorOrder label = "Validate Knobs" hosts = ["nuke"] actions = [RepairContextAction] optional = True def process(self, context): invalid = self.get_invalid(context, compute=True) if invalid: invalid_items = [ ( "Node __{node_name}__ with knob _{label}_ " "expecting _{expected}_, " "but is set to _{current}_" ).format(**i) for i in invalid ] raise PublishXmlValidationError( self, "Found knobs with invalid values:\n{}".format(invalid), formatting_data={ "invalid_items": "\n".join(invalid_items)} ) @classmethod def get_invalid(cls, context, compute=False): invalid = context.data.get("invalid_knobs", []) if compute: invalid = cls.get_invalid_knobs(context) return invalid @classmethod def get_invalid_knobs(cls, context): invalid_knobs = [] for instance in context: # Filter families. families = [instance.data["family"]] families += instance.data.get("families", []) # Get all knobs to validate. knobs = {} for family in families: # check if dot in family if "." in family: family = family.split(".")[0] # avoid families not in settings if family not in cls.knobs: continue # get presets of knobs for preset in cls.knobs[family]: knobs[preset] = cls.knobs[family][preset] # Get invalid knobs. nodes = [] for node in nuke.allNodes(): nodes.append(node) if node.Class() == "Group": node.begin() nodes.extend(iter(nuke.allNodes())) node.end() for node in nodes: for knob in node.knobs(): if knob not in knobs.keys(): continue expected = knobs[knob] if node[knob].value() != expected: invalid_knobs.append( { "node_name": node.name(), "knob": node[knob], "name": node[knob].name(), "label": node[knob].label(), "expected": expected, "current": node[knob].value() } ) context.data["invalid_knobs"] = invalid_knobs return invalid_knobs @classmethod def repair(cls, instance): invalid = cls.get_invalid(instance) for data in invalid: # TODO: will need to improve type definitions # with the new settings for knob types if isinstance(data["expected"], six.text_type): data["knob"].setValue(str(data["expected"])) continue data["knob"].setValue(data["expected"]) ================================================ FILE: openpype/hosts/nuke/plugins/publish/validate_output_resolution.py ================================================ import pyblish.api from openpype.hosts.nuke import api as napi from openpype.pipeline.publish import RepairAction from openpype.pipeline import ( PublishXmlValidationError, OptionalPyblishPluginMixin ) import nuke class ValidateOutputResolution( OptionalPyblishPluginMixin, pyblish.api.InstancePlugin ): """Validates Output Resolution. It is making sure the resolution of write's input is the same as Format definition of script in Root node. """ order = pyblish.api.ValidatorOrder optional = True families = ["render"] label = "Validate Write resolution" hosts = ["nuke"] actions = [RepairAction] missing_msg = "Missing Reformat node in render group node" resolution_msg = "Reformat is set to wrong format" def process(self, instance): if not self.is_active(instance.data): return invalid = self.get_invalid(instance) if invalid: raise PublishXmlValidationError(self, invalid) @classmethod def get_reformat(cls, instance): child_nodes = ( instance.data.get("transientData", {}).get("childNodes") or instance ) reformat = None for inode in child_nodes: if inode.Class() != "Reformat": continue reformat = inode return reformat @classmethod def get_invalid(cls, instance): def _check_resolution(instance, reformat): root_width = instance.data["resolutionWidth"] root_height = instance.data["resolutionHeight"] write_width = reformat.format().width() write_height = reformat.format().height() if (root_width != write_width) or (root_height != write_height): return None else: return True # check if reformat is in render node reformat = cls.get_reformat(instance) if not reformat: return cls.missing_msg # check if reformat is set to correct root format correct_format = _check_resolution(instance, reformat) if not correct_format: return cls.resolution_msg @classmethod def repair(cls, instance): child_nodes = ( instance.data.get("transientData", {}).get("childNodes") or instance ) invalid = cls.get_invalid(instance) grp_node = instance.data["transientData"]["node"] if cls.missing_msg == invalid: # make sure we are inside of the group node with grp_node: # find input node and select it _input = None for inode in child_nodes: if inode.Class() != "Input": continue _input = inode # add reformat node under it with napi.maintained_selection(): _input['selected'].setValue(True) _rfn = nuke.createNode("Reformat", "name Reformat01") _rfn["resize"].setValue(0) _rfn["black_outside"].setValue(1) cls.log.info("Adding reformat node") if cls.resolution_msg == invalid: reformat = cls.get_reformat(instance) reformat["format"].setValue(nuke.root()["format"].value()) cls.log.info("Fixing reformat to root.format") ================================================ FILE: openpype/hosts/nuke/plugins/publish/validate_proxy_mode.py ================================================ import pyblish import nuke from openpype.pipeline import PublishXmlValidationError class FixProxyMode(pyblish.api.Action): """ Togger off proxy switch OFF """ label = "Repair" icon = "wrench" on = "failed" def process(self, context, plugin): rootNode = nuke.root() rootNode["proxy"].setValue(False) class ValidateProxyMode(pyblish.api.ContextPlugin): """Validate active proxy mode""" order = pyblish.api.ValidatorOrder label = "Validate Proxy Mode" hosts = ["nuke"] actions = [FixProxyMode] def process(self, context): rootNode = nuke.root() isProxy = rootNode["proxy"].value() if isProxy: raise PublishXmlValidationError( self, "Proxy mode should be toggled OFF" ) ================================================ FILE: openpype/hosts/nuke/plugins/publish/validate_rendered_frames.py ================================================ import os import pyblish.api import clique from openpype.pipeline import PublishXmlValidationError from openpype.pipeline.publish import get_errored_instances_from_context class RepairActionBase(pyblish.api.Action): on = "failed" icon = "wrench" @staticmethod def get_instance(context, plugin): # Get the errored instances return get_errored_instances_from_context(context, plugin=plugin) def repair_knob(self, context, instances, state): create_context = context.data["create_context"] for instance in instances: # Reset the render knob instance_id = instance.data.get("instance_id") created_instance = create_context.get_instance_by_id( instance_id ) created_instance.creator_attributes["render_target"] = state self.log.info("Rendering toggled to `{}`".format(state)) create_context.save_changes() class RepairCollectionActionToLocal(RepairActionBase): label = "Repair - rerender with \"Local\"" def process(self, context, plugin): instances = self.get_instance(context, plugin) self.repair_knob(context, instances, "local") class RepairCollectionActionToFarm(RepairActionBase): label = "Repair - rerender with \"On farm\"" def process(self, context, plugin): instances = self.get_instance(context, plugin) self.repair_knob(context, instances, "farm") class ValidateRenderedFrames(pyblish.api.InstancePlugin): """ Validates file output. """ order = pyblish.api.ValidatorOrder + 0.1 families = ["render", "prerender", "still"] label = "Validate rendered frame" hosts = ["nuke", "nukestudio"] actions = [RepairCollectionActionToLocal, RepairCollectionActionToFarm] def process(self, instance): node = instance.data["transientData"]["node"] f_data = { "node_name": node.name() } for repre in instance.data["representations"]: if not repre.get("files"): msg = ("no frames were collected, " "you need to render them.\n" "Check properties of write node (group) and" "select 'Local' option in 'Publish' dropdown.") self.log.error(msg) raise PublishXmlValidationError( self, msg, formatting_data=f_data) if isinstance(repre["files"], str): return collections, remainder = clique.assemble(repre["files"]) self.log.debug("collections: {}".format(str(collections))) self.log.debug("remainder: {}".format(str(remainder))) collection = collections[0] f_start_h = instance.data["frameStartHandle"] f_end_h = instance.data["frameEndHandle"] frame_length = int(f_end_h - f_start_h + 1) if frame_length != 1: if len(collections) != 1: msg = "There are multiple collections in the folder" self.log.error(msg) raise PublishXmlValidationError( self, msg, formatting_data=f_data) if not collection.is_contiguous(): msg = "Some frames appear to be missing" self.log.error(msg) raise PublishXmlValidationError( self, msg, formatting_data=f_data) collected_frames_len = len(collection.indexes) coll_start = min(collection.indexes) coll_end = max(collection.indexes) self.log.debug("frame_length: {}".format(frame_length)) self.log.debug("collected_frames_len: {}".format( collected_frames_len)) self.log.debug("f_start_h-f_end_h: {}-{}".format( f_start_h, f_end_h)) self.log.debug( "coll_start-coll_end: {}-{}".format(coll_start, coll_end)) self.log.debug( "len(collection.indexes): {}".format(collected_frames_len) ) if ("slate" in instance.data["families"]) \ and (frame_length != collected_frames_len): collected_frames_len -= 1 f_start_h += 1 if ( collected_frames_len != frame_length and coll_start <= f_start_h and coll_end >= f_end_h ): raise PublishXmlValidationError( self, ( "{} missing frames. Use repair to " "render all frames" ).format(__name__), formatting_data=f_data ) instance.data["collection"] = collection return ================================================ FILE: openpype/hosts/nuke/plugins/publish/validate_script_attributes.py ================================================ from copy import deepcopy import pyblish.api from openpype.pipeline import ( PublishXmlValidationError, OptionalPyblishPluginMixin ) from openpype.pipeline.publish import RepairAction from openpype.hosts.nuke.api.lib import ( WorkfileSettings ) class ValidateScriptAttributes( OptionalPyblishPluginMixin, pyblish.api.InstancePlugin ): """ Validates file output. """ order = pyblish.api.ValidatorOrder + 0.1 families = ["workfile"] label = "Validate script attributes" hosts = ["nuke"] optional = True actions = [RepairAction] def process(self, instance): if not self.is_active(instance.data): return script_data = deepcopy(instance.context.data["scriptData"]) asset = instance.data["assetEntity"] # These attributes will be checked attributes = [ "fps", "frameStart", "frameEnd", "resolutionWidth", "resolutionHeight", "handleStart", "handleEnd" ] # get only defined attributes from asset data asset_attributes = { attr: asset["data"][attr] for attr in attributes if attr in asset["data"] } # fix frame values to include handles asset_attributes["fps"] = float("{0:.4f}".format( asset_attributes["fps"])) script_data["fps"] = float("{0:.4f}".format( script_data["fps"])) # Compare asset's values Nukescript X Database not_matching = [] for attr in attributes: self.log.debug( "Asset vs Script attribute \"{}\": {}, {}".format( attr, asset_attributes[attr], script_data[attr] ) ) if asset_attributes[attr] != script_data[attr]: not_matching.append({ "name": attr, "expected": asset_attributes[attr], "actual": script_data[attr] }) # Raise error if not matching if not_matching: msg = "Following attributes are not set correctly: \n{}" attrs_wrong_str = "\n".join([ ( "`{0}` is set to `{1}`, " "but should be set to `{2}`" ).format(at["name"], at["actual"], at["expected"]) for at in not_matching ]) attrs_wrong_html = " Knobs values ## Invalid node's knobs values Following write node knobs needs to be repaired: {xml_msg} ### How to repair? 1. Use Repair button. 2. Hit Reload button on the publisher. Legacy knob types ## Knobs are in obsolete configuration Settings needs to be fixed. ### How to repair? Contact your supervisor or fix it in project settings at 'project_settings/nuke/imageio/nodes/requiredNodes' at knobs. Each '__legacy__' type has to be defined accordingly to its type.
".join([ ( "-- __{0}__ is set to __{1}__, " "but should be set to __{2}__" ).format(at["name"], at["actual"], at["expected"]) for at in not_matching ]) raise PublishXmlValidationError( self, msg.format(attrs_wrong_str), formatting_data={ "failed_attributes": attrs_wrong_html } ) @classmethod def repair(cls, instance): cls.log.debug("__ repairing instance: {}".format(instance)) WorkfileSettings().set_context_settings() ================================================ FILE: openpype/hosts/nuke/plugins/publish/validate_write_nodes.py ================================================ from collections import defaultdict import pyblish.api from openpype.pipeline.publish import get_errored_instances_from_context from openpype.hosts.nuke.api.lib import ( get_write_node_template_attr, set_node_knobs_from_settings, color_gui_to_int ) from openpype.pipeline.publish import ( PublishXmlValidationError, OptionalPyblishPluginMixin ) class RepairNukeWriteNodeAction(pyblish.api.Action): label = "Repair" on = "failed" icon = "wrench" def process(self, context, plugin): instances = get_errored_instances_from_context(context) for instance in instances: child_nodes = ( instance.data.get("transientData", {}).get("childNodes") or instance ) write_group_node = instance.data["transientData"]["node"] # get write node from inside of group write_node = None for x in child_nodes: if x.Class() == "Write": write_node = x correct_data = get_write_node_template_attr(write_group_node) set_node_knobs_from_settings(write_node, correct_data["knobs"]) self.log.debug("Node attributes were fixed") class ValidateNukeWriteNode( OptionalPyblishPluginMixin, pyblish.api.InstancePlugin ): """ Validate Write node's knobs. Compare knobs on write node inside the render group with settings. At the moment supporting only `file` knob. """ order = pyblish.api.ValidatorOrder optional = True families = ["render"] label = "Validate write node" actions = [RepairNukeWriteNodeAction] hosts = ["nuke"] def process(self, instance): if not self.is_active(instance.data): return child_nodes = ( instance.data.get("transientData", {}).get("childNodes") or instance ) write_group_node = instance.data["transientData"]["node"] # get write node from inside of group write_node = None for x in child_nodes: if x.Class() == "Write": write_node = x if write_node is None: return correct_data = get_write_node_template_attr(write_group_node) check = [] # Collect key values of same type in a list. values_by_name = defaultdict(list) for knob_data in correct_data["knobs"]: values_by_name[knob_data["name"]].append(knob_data["value"]) for knob_data in correct_data["knobs"]: knob_type = knob_data["type"] if ( knob_type == "__legacy__" ): raise PublishXmlValidationError( self, ( "Please update data in settings 'project_settings" "/nuke/imageio/nodes/requiredNodes'" ), key="legacy" ) key = knob_data["name"] values = values_by_name[key] node_value = write_node[key].value() # fix type differences fixed_values = [] for value in values: if type(node_value) in (int, float): try: if isinstance(value, list): value = color_gui_to_int(value) else: value = float(value) node_value = float(node_value) except ValueError: value = str(value) else: value = str(value) node_value = str(node_value) fixed_values.append(value) if ( node_value not in fixed_values and key != "file" and key != "tile_color" ): check.append([key, fixed_values, write_node[key].value()]) if check: self._make_error(check) def _make_error(self, check): # sourcery skip: merge-assign-and-aug-assign, move-assign-in-block dbg_msg = "Write node's knobs values are not correct!\n" msg_add = "Knob '{0}' > Expected: `{1}` > Current: `{2}`" details = [ msg_add.format(item[0], item[1], item[2]) for item in check ] xml_msg = "
".join(details) dbg_msg += "\n\t".join(details) raise PublishXmlValidationError( self, dbg_msg, formatting_data={"xml_msg": xml_msg} ) ================================================ FILE: openpype/hosts/nuke/startup/__init__.py ================================================ ================================================ FILE: openpype/hosts/nuke/startup/clear_rendered.py ================================================ import os from openpype.lib import Logger def clear_rendered(dir_path): log = Logger.get_logger(__name__) for _f in os.listdir(dir_path): _f_path = os.path.join(dir_path, _f) log.info("Removing: `{}`".format(_f_path)) os.remove(_f_path) ================================================ FILE: openpype/hosts/nuke/startup/custom_write_node.py ================================================ """ OpenPype custom script for setting up write nodes for non-publish """ import os import nuke import nukescripts from openpype.pipeline import Anatomy from openpype.hosts.nuke.api.lib import ( set_node_knobs_from_settings, get_nuke_imageio_settings ) temp_rendering_path_template = ( "{work}/renders/nuke/{subset}/{subset}.{frame}.{ext}") knobs_setting = { "knobs": [ { "type": "text", "name": "file_type", "value": "exr" }, { "type": "text", "name": "datatype", "value": "16 bit half" }, { "type": "text", "name": "compression", "value": "Zip (1 scanline)" }, { "type": "bool", "name": "autocrop", "value": True }, { "type": "color_gui", "name": "tile_color", "value": [ 186, 35, 35, 255 ] }, { "type": "text", "name": "channels", "value": "rgb" }, { "type": "bool", "name": "create_directories", "value": True } ] } class WriteNodeKnobSettingPanel(nukescripts.PythonPanel): """ Write Node's Knobs Settings Panel """ def __init__(self): nukescripts.PythonPanel.__init__(self, "Set Knobs Value(Write Node)") preset_name, _ = self.get_node_knobs_setting() # create knobs self.selected_preset_name = nuke.Enumeration_Knob( 'preset_selector', 'presets', preset_name) # add knobs to panel self.addKnob(self.selected_preset_name) def process(self): """ Process the panel values. """ write_selected_nodes = [ selected_nodes for selected_nodes in nuke.selectedNodes() if selected_nodes.Class() == "Write"] selected_preset = self.selected_preset_name.value() ext = None knobs = knobs_setting["knobs"] preset_name, node_knobs_presets = ( self.get_node_knobs_setting(selected_preset) ) if selected_preset and preset_name: if not node_knobs_presets: nuke.message( "No knobs value found in subset group.." "\nDefault setting will be used..") else: knobs = node_knobs_presets ext_knob_list = [knob for knob in knobs if knob["name"] == "file_type"] if not ext_knob_list: nuke.message( "ERROR: No file type found in the subset's knobs." "\nPlease add one to complete setting up the node") return else: for knob in ext_knob_list: ext = knob["value"] anatomy = Anatomy() frame_padding = int( anatomy.templates["render"].get( "frame_padding" ) ) for write_node in write_selected_nodes: # data for mapping the path data = { "work": os.getenv("AVALON_WORKDIR"), "subset": write_node["name"].value(), "frame": "#" * frame_padding, "ext": ext } file_path = temp_rendering_path_template.format(**data) file_path = file_path.replace("\\", "/") write_node["file"].setValue(file_path) set_node_knobs_from_settings(write_node, knobs) def get_node_knobs_setting(self, selected_preset=None): preset_name = [] knobs_nodes = [] settings = [ node_settings for node_settings in get_nuke_imageio_settings()["nodes"]["overrideNodes"] if node_settings["nukeNodeClass"] == "Write" and node_settings["subsets"] ] if not settings: return for i, _ in enumerate(settings): if selected_preset in settings[i]["subsets"]: knobs_nodes = settings[i]["knobs"] for setting in settings: for subset in setting["subsets"]: preset_name.append(subset) return preset_name, knobs_nodes def main(): p_ = WriteNodeKnobSettingPanel() if p_.showModalDialog(): print(p_.process()) ================================================ FILE: openpype/hosts/nuke/startup/frame_setting_for_read_nodes.py ================================================ """ OpenPype custom script for resetting read nodes start frame values """ import nuke import nukescripts class FrameSettingsPanel(nukescripts.PythonPanel): """ Frame Settings Panel """ def __init__(self): nukescripts.PythonPanel.__init__(self, "Set Frame Start (Read Node)") # create knobs self.frame = nuke.Int_Knob( 'frame', 'Frame Number') self.selected = nuke.Boolean_Knob("selection") # add knobs to panel self.addKnob(self.selected) self.addKnob(self.frame) # set values self.selected.setValue(False) self.frame.setValue(nuke.root().firstFrame()) def process(self): """ Process the panel values. """ # get values frame = self.frame.value() if self.selected.value(): # selected nodes processing if not nuke.selectedNodes(): return for rn_ in nuke.selectedNodes(): if rn_.Class() != "Read": continue rn_["frame_mode"].setValue("start_at") rn_["frame"].setValue(str(frame)) else: # all nodes processing for rn_ in nuke.allNodes(filter="Read"): rn_["frame_mode"].setValue("start_at") rn_["frame"].setValue(str(frame)) def main(): p_ = FrameSettingsPanel() if p_.showModalDialog(): print(p_.process()) ================================================ FILE: openpype/hosts/nuke/startup/menu.py ================================================ from openpype.pipeline import install_host from openpype.hosts.nuke.api import NukeHost host = NukeHost() install_host(host) ================================================ FILE: openpype/hosts/nuke/startup/write_to_read.py ================================================ import re import os import glob import nuke from openpype.lib import Logger log = Logger.get_logger(__name__) SINGLE_FILE_FORMATS = ['avi', 'mp4', 'mxf', 'mov', 'mpg', 'mpeg', 'wmv', 'm4v', 'm2v'] def evaluate_filepath_new( k_value, k_eval, project_dir, first_frame, allow_relative): # get combined relative path combined_relative_path = None if k_eval is not None and project_dir is not None: combined_relative_path = os.path.abspath( os.path.join(project_dir, k_eval)) combined_relative_path = combined_relative_path.replace('\\', '/') filetype = combined_relative_path.split('.')[-1] frame_number = re.findall(r'\d+', combined_relative_path)[-1] basename = combined_relative_path[: combined_relative_path.rfind( frame_number)] filepath_glob = basename + '*' + filetype glob_search_results = glob.glob(filepath_glob) if len(glob_search_results) <= 0: combined_relative_path = None try: # k_value = k_value % first_frame if os.path.isdir(os.path.basename(k_value)): # doesn't check for file, only parent dir filepath = k_value elif os.path.exists(k_eval): filepath = k_eval elif not isinstance(project_dir, type(None)) and \ not isinstance(combined_relative_path, type(None)): filepath = combined_relative_path filepath = os.path.abspath(filepath) except Exception as E: log.error("Cannot create Read node. Perhaps it needs to be \ rendered first :) Error: `{}`".format(E)) return None filepath = filepath.replace('\\', '/') # assumes last number is a sequence counter current_frame = re.findall(r'\d+', filepath)[-1] padding = len(current_frame) basename = filepath[: filepath.rfind(current_frame)] filetype = filepath.split('.')[-1] # sequence or not? if filetype in SINGLE_FILE_FORMATS: pass else: # Image sequence needs hashes # to do still with no number not handled filepath = basename + '#' * padding + '.' + filetype # relative path? make it relative again if allow_relative: if (not isinstance(project_dir, type(None))) and project_dir != "": filepath = filepath.replace(project_dir, '.') # get first and last frame from disk frames = [] firstframe = 0 lastframe = 0 filepath_glob = basename + '*' + filetype glob_search_results = glob.glob(filepath_glob) for f in glob_search_results: frame = re.findall(r'\d+', f)[-1] frames.append(frame) frames = sorted(frames) firstframe = frames[0] lastframe = frames[len(frames) - 1] if int(lastframe) < 0: lastframe = firstframe return filepath, firstframe, lastframe def create_read_node(ndata, comp_start): read = nuke.createNode('Read', 'file "' + ndata['filepath'] + '"') read.knob('colorspace').setValue(int(ndata['colorspace'])) read.knob('raw').setValue(ndata['rawdata']) read.knob('first').setValue(int(ndata['firstframe'])) read.knob('last').setValue(int(ndata['lastframe'])) read.knob('origfirst').setValue(int(ndata['firstframe'])) read.knob('origlast').setValue(int(ndata['lastframe'])) if comp_start == int(ndata['firstframe']): read.knob('frame_mode').setValue("1") read.knob('frame').setValue(str(comp_start)) else: read.knob('frame_mode').setValue("0") read.knob('xpos').setValue(ndata['new_xpos']) read.knob('ypos').setValue(ndata['new_ypos']) nuke.inputs(read, 0) return def write_to_read(gn, allow_relative=False): comp_start = nuke.Root().knob('first_frame').value() project_dir = nuke.Root().knob('project_directory').getValue() if not os.path.exists(project_dir): project_dir = nuke.Root().knob('project_directory').evaluate() group_read_nodes = [] with gn: height = gn.screenHeight() # get group height and position new_xpos = int(gn.knob('xpos').value()) new_ypos = int(gn.knob('ypos').value()) + height + 20 group_writes = [n for n in nuke.allNodes() if n.Class() == "Write"] if group_writes != []: # there can be only 1 write node, taking first n = group_writes[0] if n.knob('file') is not None: myfile, firstFrame, lastFrame = evaluate_filepath_new( n.knob('file').getValue(), n.knob('file').evaluate(), project_dir, comp_start, allow_relative ) if not myfile: return # get node data ndata = { 'filepath': myfile, 'firstframe': int(firstFrame), 'lastframe': int(lastFrame), 'new_xpos': new_xpos, 'new_ypos': new_ypos, 'colorspace': n.knob('colorspace').getValue(), 'rawdata': n.knob('raw').value(), 'write_frame_mode': str(n.knob('frame_mode').value()), 'write_frame': n.knob('frame').value() } group_read_nodes.append(ndata) # create reads in one go for oneread in group_read_nodes: # create read node create_read_node(oneread, comp_start) ================================================ FILE: openpype/hosts/nuke/vendor/google/protobuf/__init__.py ================================================ # Protocol Buffers - Google's data interchange format # Copyright 2008 Google Inc. All rights reserved. # https://developers.google.com/protocol-buffers/ # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are # met: # # * Redistributions of source code must retain the above copyright # notice, this list of conditions and the following disclaimer. # * Redistributions in binary form must reproduce the above # copyright notice, this list of conditions and the following disclaimer # in the documentation and/or other materials provided with the # distribution. # * Neither the name of Google Inc. nor the names of its # contributors may be used to endorse or promote products derived from # this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. # Copyright 2007 Google Inc. All Rights Reserved. __version__ = '3.20.1' ================================================ FILE: openpype/hosts/nuke/vendor/google/protobuf/any_pb2.py ================================================ # -*- coding: utf-8 -*- # Generated by the protocol buffer compiler. DO NOT EDIT! # source: google/protobuf/any.proto """Generated protocol buffer code.""" from google.protobuf.internal import builder as _builder from google.protobuf import descriptor as _descriptor from google.protobuf import descriptor_pool as _descriptor_pool from google.protobuf import symbol_database as _symbol_database # @@protoc_insertion_point(imports) _sym_db = _symbol_database.Default() DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x19google/protobuf/any.proto\x12\x0fgoogle.protobuf\"&\n\x03\x41ny\x12\x10\n\x08type_url\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\x0c\x42v\n\x13\x63om.google.protobufB\x08\x41nyProtoP\x01Z,google.golang.org/protobuf/types/known/anypb\xa2\x02\x03GPB\xaa\x02\x1eGoogle.Protobuf.WellKnownTypesb\x06proto3') _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals()) _builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'google.protobuf.any_pb2', globals()) if _descriptor._USE_C_DESCRIPTORS == False: DESCRIPTOR._options = None DESCRIPTOR._serialized_options = b'\n\023com.google.protobufB\010AnyProtoP\001Z,google.golang.org/protobuf/types/known/anypb\242\002\003GPB\252\002\036Google.Protobuf.WellKnownTypes' _ANY._serialized_start=46 _ANY._serialized_end=84 # @@protoc_insertion_point(module_scope) ================================================ FILE: openpype/hosts/nuke/vendor/google/protobuf/api_pb2.py ================================================ # -*- coding: utf-8 -*- # Generated by the protocol buffer compiler. DO NOT EDIT! # source: google/protobuf/api.proto """Generated protocol buffer code.""" from google.protobuf.internal import builder as _builder from google.protobuf import descriptor as _descriptor from google.protobuf import descriptor_pool as _descriptor_pool from google.protobuf import symbol_database as _symbol_database # @@protoc_insertion_point(imports) _sym_db = _symbol_database.Default() from google.protobuf import source_context_pb2 as google_dot_protobuf_dot_source__context__pb2 from google.protobuf import type_pb2 as google_dot_protobuf_dot_type__pb2 DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x19google/protobuf/api.proto\x12\x0fgoogle.protobuf\x1a$google/protobuf/source_context.proto\x1a\x1agoogle/protobuf/type.proto\"\x81\x02\n\x03\x41pi\x12\x0c\n\x04name\x18\x01 \x01(\t\x12(\n\x07methods\x18\x02 \x03(\x0b\x32\x17.google.protobuf.Method\x12(\n\x07options\x18\x03 \x03(\x0b\x32\x17.google.protobuf.Option\x12\x0f\n\x07version\x18\x04 \x01(\t\x12\x36\n\x0esource_context\x18\x05 \x01(\x0b\x32\x1e.google.protobuf.SourceContext\x12&\n\x06mixins\x18\x06 \x03(\x0b\x32\x16.google.protobuf.Mixin\x12\'\n\x06syntax\x18\x07 \x01(\x0e\x32\x17.google.protobuf.Syntax\"\xd5\x01\n\x06Method\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x18\n\x10request_type_url\x18\x02 \x01(\t\x12\x19\n\x11request_streaming\x18\x03 \x01(\x08\x12\x19\n\x11response_type_url\x18\x04 \x01(\t\x12\x1a\n\x12response_streaming\x18\x05 \x01(\x08\x12(\n\x07options\x18\x06 \x03(\x0b\x32\x17.google.protobuf.Option\x12\'\n\x06syntax\x18\x07 \x01(\x0e\x32\x17.google.protobuf.Syntax\"#\n\x05Mixin\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0c\n\x04root\x18\x02 \x01(\tBv\n\x13\x63om.google.protobufB\x08\x41piProtoP\x01Z,google.golang.org/protobuf/types/known/apipb\xa2\x02\x03GPB\xaa\x02\x1eGoogle.Protobuf.WellKnownTypesb\x06proto3') _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals()) _builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'google.protobuf.api_pb2', globals()) if _descriptor._USE_C_DESCRIPTORS == False: DESCRIPTOR._options = None DESCRIPTOR._serialized_options = b'\n\023com.google.protobufB\010ApiProtoP\001Z,google.golang.org/protobuf/types/known/apipb\242\002\003GPB\252\002\036Google.Protobuf.WellKnownTypes' _API._serialized_start=113 _API._serialized_end=370 _METHOD._serialized_start=373 _METHOD._serialized_end=586 _MIXIN._serialized_start=588 _MIXIN._serialized_end=623 # @@protoc_insertion_point(module_scope) ================================================ FILE: openpype/hosts/nuke/vendor/google/protobuf/compiler/__init__.py ================================================ ================================================ FILE: openpype/hosts/nuke/vendor/google/protobuf/compiler/plugin_pb2.py ================================================ # -*- coding: utf-8 -*- # Generated by the protocol buffer compiler. DO NOT EDIT! # source: google/protobuf/compiler/plugin.proto """Generated protocol buffer code.""" from google.protobuf.internal import builder as _builder from google.protobuf import descriptor as _descriptor from google.protobuf import descriptor_pool as _descriptor_pool from google.protobuf import symbol_database as _symbol_database # @@protoc_insertion_point(imports) _sym_db = _symbol_database.Default() from google.protobuf import descriptor_pb2 as google_dot_protobuf_dot_descriptor__pb2 DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n%google/protobuf/compiler/plugin.proto\x12\x18google.protobuf.compiler\x1a google/protobuf/descriptor.proto\"F\n\x07Version\x12\r\n\x05major\x18\x01 \x01(\x05\x12\r\n\x05minor\x18\x02 \x01(\x05\x12\r\n\x05patch\x18\x03 \x01(\x05\x12\x0e\n\x06suffix\x18\x04 \x01(\t\"\xba\x01\n\x14\x43odeGeneratorRequest\x12\x18\n\x10\x66ile_to_generate\x18\x01 \x03(\t\x12\x11\n\tparameter\x18\x02 \x01(\t\x12\x38\n\nproto_file\x18\x0f \x03(\x0b\x32$.google.protobuf.FileDescriptorProto\x12;\n\x10\x63ompiler_version\x18\x03 \x01(\x0b\x32!.google.protobuf.compiler.Version\"\xc1\x02\n\x15\x43odeGeneratorResponse\x12\r\n\x05\x65rror\x18\x01 \x01(\t\x12\x1a\n\x12supported_features\x18\x02 \x01(\x04\x12\x42\n\x04\x66ile\x18\x0f \x03(\x0b\x32\x34.google.protobuf.compiler.CodeGeneratorResponse.File\x1a\x7f\n\x04\x46ile\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x17\n\x0finsertion_point\x18\x02 \x01(\t\x12\x0f\n\x07\x63ontent\x18\x0f \x01(\t\x12?\n\x13generated_code_info\x18\x10 \x01(\x0b\x32\".google.protobuf.GeneratedCodeInfo\"8\n\x07\x46\x65\x61ture\x12\x10\n\x0c\x46\x45\x41TURE_NONE\x10\x00\x12\x1b\n\x17\x46\x45\x41TURE_PROTO3_OPTIONAL\x10\x01\x42W\n\x1c\x63om.google.protobuf.compilerB\x0cPluginProtosZ)google.golang.org/protobuf/types/pluginpb') _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals()) _builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'google.protobuf.compiler.plugin_pb2', globals()) if _descriptor._USE_C_DESCRIPTORS == False: DESCRIPTOR._options = None DESCRIPTOR._serialized_options = b'\n\034com.google.protobuf.compilerB\014PluginProtosZ)google.golang.org/protobuf/types/pluginpb' _VERSION._serialized_start=101 _VERSION._serialized_end=171 _CODEGENERATORREQUEST._serialized_start=174 _CODEGENERATORREQUEST._serialized_end=360 _CODEGENERATORRESPONSE._serialized_start=363 _CODEGENERATORRESPONSE._serialized_end=684 _CODEGENERATORRESPONSE_FILE._serialized_start=499 _CODEGENERATORRESPONSE_FILE._serialized_end=626 _CODEGENERATORRESPONSE_FEATURE._serialized_start=628 _CODEGENERATORRESPONSE_FEATURE._serialized_end=684 # @@protoc_insertion_point(module_scope) ================================================ FILE: openpype/hosts/nuke/vendor/google/protobuf/descriptor.py ================================================ # Protocol Buffers - Google's data interchange format # Copyright 2008 Google Inc. All rights reserved. # https://developers.google.com/protocol-buffers/ # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are # met: # # * Redistributions of source code must retain the above copyright # notice, this list of conditions and the following disclaimer. # * Redistributions in binary form must reproduce the above # copyright notice, this list of conditions and the following disclaimer # in the documentation and/or other materials provided with the # distribution. # * Neither the name of Google Inc. nor the names of its # contributors may be used to endorse or promote products derived from # this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. """Descriptors essentially contain exactly the information found in a .proto file, in types that make this information accessible in Python. """ __author__ = 'robinson@google.com (Will Robinson)' import threading import warnings from google.protobuf.internal import api_implementation _USE_C_DESCRIPTORS = False if api_implementation.Type() == 'cpp': # Used by MakeDescriptor in cpp mode import binascii import os from google.protobuf.pyext import _message _USE_C_DESCRIPTORS = True class Error(Exception): """Base error for this module.""" class TypeTransformationError(Error): """Error transforming between python proto type and corresponding C++ type.""" if _USE_C_DESCRIPTORS: # This metaclass allows to override the behavior of code like # isinstance(my_descriptor, FieldDescriptor) # and make it return True when the descriptor is an instance of the extension # type written in C++. class DescriptorMetaclass(type): def __instancecheck__(cls, obj): if super(DescriptorMetaclass, cls).__instancecheck__(obj): return True if isinstance(obj, cls._C_DESCRIPTOR_CLASS): return True return False else: # The standard metaclass; nothing changes. DescriptorMetaclass = type class _Lock(object): """Wrapper class of threading.Lock(), which is allowed by 'with'.""" def __new__(cls): self = object.__new__(cls) self._lock = threading.Lock() # pylint: disable=protected-access return self def __enter__(self): self._lock.acquire() def __exit__(self, exc_type, exc_value, exc_tb): self._lock.release() _lock = threading.Lock() def _Deprecated(name): if _Deprecated.count > 0: _Deprecated.count -= 1 warnings.warn( 'Call to deprecated create function %s(). Note: Create unlinked ' 'descriptors is going to go away. Please use get/find descriptors from ' 'generated code or query the descriptor_pool.' % name, category=DeprecationWarning, stacklevel=3) # Deprecated warnings will print 100 times at most which should be enough for # users to notice and do not cause timeout. _Deprecated.count = 100 _internal_create_key = object() class DescriptorBase(metaclass=DescriptorMetaclass): """Descriptors base class. This class is the base of all descriptor classes. It provides common options related functionality. Attributes: has_options: True if the descriptor has non-default options. Usually it is not necessary to read this -- just call GetOptions() which will happily return the default instance. However, it's sometimes useful for efficiency, and also useful inside the protobuf implementation to avoid some bootstrapping issues. """ if _USE_C_DESCRIPTORS: # The class, or tuple of classes, that are considered as "virtual # subclasses" of this descriptor class. _C_DESCRIPTOR_CLASS = () def __init__(self, options, serialized_options, options_class_name): """Initialize the descriptor given its options message and the name of the class of the options message. The name of the class is required in case the options message is None and has to be created. """ self._options = options self._options_class_name = options_class_name self._serialized_options = serialized_options # Does this descriptor have non-default options? self.has_options = (options is not None) or (serialized_options is not None) def _SetOptions(self, options, options_class_name): """Sets the descriptor's options This function is used in generated proto2 files to update descriptor options. It must not be used outside proto2. """ self._options = options self._options_class_name = options_class_name # Does this descriptor have non-default options? self.has_options = options is not None def GetOptions(self): """Retrieves descriptor options. This method returns the options set or creates the default options for the descriptor. """ if self._options: return self._options from google.protobuf import descriptor_pb2 try: options_class = getattr(descriptor_pb2, self._options_class_name) except AttributeError: raise RuntimeError('Unknown options class name %s!' % (self._options_class_name)) with _lock: if self._serialized_options is None: self._options = options_class() else: self._options = _ParseOptions(options_class(), self._serialized_options) return self._options class _NestedDescriptorBase(DescriptorBase): """Common class for descriptors that can be nested.""" def __init__(self, options, options_class_name, name, full_name, file, containing_type, serialized_start=None, serialized_end=None, serialized_options=None): """Constructor. Args: options: Protocol message options or None to use default message options. options_class_name (str): The class name of the above options. name (str): Name of this protocol message type. full_name (str): Fully-qualified name of this protocol message type, which will include protocol "package" name and the name of any enclosing types. file (FileDescriptor): Reference to file info. containing_type: if provided, this is a nested descriptor, with this descriptor as parent, otherwise None. serialized_start: The start index (inclusive) in block in the file.serialized_pb that describes this descriptor. serialized_end: The end index (exclusive) in block in the file.serialized_pb that describes this descriptor. serialized_options: Protocol message serialized options or None. """ super(_NestedDescriptorBase, self).__init__( options, serialized_options, options_class_name) self.name = name # TODO(falk): Add function to calculate full_name instead of having it in # memory? self.full_name = full_name self.file = file self.containing_type = containing_type self._serialized_start = serialized_start self._serialized_end = serialized_end def CopyToProto(self, proto): """Copies this to the matching proto in descriptor_pb2. Args: proto: An empty proto instance from descriptor_pb2. Raises: Error: If self couldn't be serialized, due to to few constructor arguments. """ if (self.file is not None and self._serialized_start is not None and self._serialized_end is not None): proto.ParseFromString(self.file.serialized_pb[ self._serialized_start:self._serialized_end]) else: raise Error('Descriptor does not contain serialization.') class Descriptor(_NestedDescriptorBase): """Descriptor for a protocol message type. Attributes: name (str): Name of this protocol message type. full_name (str): Fully-qualified name of this protocol message type, which will include protocol "package" name and the name of any enclosing types. containing_type (Descriptor): Reference to the descriptor of the type containing us, or None if this is top-level. fields (list[FieldDescriptor]): Field descriptors for all fields in this type. fields_by_number (dict(int, FieldDescriptor)): Same :class:`FieldDescriptor` objects as in :attr:`fields`, but indexed by "number" attribute in each FieldDescriptor. fields_by_name (dict(str, FieldDescriptor)): Same :class:`FieldDescriptor` objects as in :attr:`fields`, but indexed by "name" attribute in each :class:`FieldDescriptor`. nested_types (list[Descriptor]): Descriptor references for all protocol message types nested within this one. nested_types_by_name (dict(str, Descriptor)): Same Descriptor objects as in :attr:`nested_types`, but indexed by "name" attribute in each Descriptor. enum_types (list[EnumDescriptor]): :class:`EnumDescriptor` references for all enums contained within this type. enum_types_by_name (dict(str, EnumDescriptor)): Same :class:`EnumDescriptor` objects as in :attr:`enum_types`, but indexed by "name" attribute in each EnumDescriptor. enum_values_by_name (dict(str, EnumValueDescriptor)): Dict mapping from enum value name to :class:`EnumValueDescriptor` for that value. extensions (list[FieldDescriptor]): All extensions defined directly within this message type (NOT within a nested type). extensions_by_name (dict(str, FieldDescriptor)): Same FieldDescriptor objects as :attr:`extensions`, but indexed by "name" attribute of each FieldDescriptor. is_extendable (bool): Does this type define any extension ranges? oneofs (list[OneofDescriptor]): The list of descriptors for oneof fields in this message. oneofs_by_name (dict(str, OneofDescriptor)): Same objects as in :attr:`oneofs`, but indexed by "name" attribute. file (FileDescriptor): Reference to file descriptor. """ if _USE_C_DESCRIPTORS: _C_DESCRIPTOR_CLASS = _message.Descriptor def __new__( cls, name=None, full_name=None, filename=None, containing_type=None, fields=None, nested_types=None, enum_types=None, extensions=None, options=None, serialized_options=None, is_extendable=True, extension_ranges=None, oneofs=None, file=None, # pylint: disable=redefined-builtin serialized_start=None, serialized_end=None, syntax=None, create_key=None): _message.Message._CheckCalledFromGeneratedFile() return _message.default_pool.FindMessageTypeByName(full_name) # NOTE(tmarek): The file argument redefining a builtin is nothing we can # fix right now since we don't know how many clients already rely on the # name of the argument. def __init__(self, name, full_name, filename, containing_type, fields, nested_types, enum_types, extensions, options=None, serialized_options=None, is_extendable=True, extension_ranges=None, oneofs=None, file=None, serialized_start=None, serialized_end=None, # pylint: disable=redefined-builtin syntax=None, create_key=None): """Arguments to __init__() are as described in the description of Descriptor fields above. Note that filename is an obsolete argument, that is not used anymore. Please use file.name to access this as an attribute. """ if create_key is not _internal_create_key: _Deprecated('Descriptor') super(Descriptor, self).__init__( options, 'MessageOptions', name, full_name, file, containing_type, serialized_start=serialized_start, serialized_end=serialized_end, serialized_options=serialized_options) # We have fields in addition to fields_by_name and fields_by_number, # so that: # 1. Clients can index fields by "order in which they're listed." # 2. Clients can easily iterate over all fields with the terse # syntax: for f in descriptor.fields: ... self.fields = fields for field in self.fields: field.containing_type = self self.fields_by_number = dict((f.number, f) for f in fields) self.fields_by_name = dict((f.name, f) for f in fields) self._fields_by_camelcase_name = None self.nested_types = nested_types for nested_type in nested_types: nested_type.containing_type = self self.nested_types_by_name = dict((t.name, t) for t in nested_types) self.enum_types = enum_types for enum_type in self.enum_types: enum_type.containing_type = self self.enum_types_by_name = dict((t.name, t) for t in enum_types) self.enum_values_by_name = dict( (v.name, v) for t in enum_types for v in t.values) self.extensions = extensions for extension in self.extensions: extension.extension_scope = self self.extensions_by_name = dict((f.name, f) for f in extensions) self.is_extendable = is_extendable self.extension_ranges = extension_ranges self.oneofs = oneofs if oneofs is not None else [] self.oneofs_by_name = dict((o.name, o) for o in self.oneofs) for oneof in self.oneofs: oneof.containing_type = self self.syntax = syntax or "proto2" @property def fields_by_camelcase_name(self): """Same FieldDescriptor objects as in :attr:`fields`, but indexed by :attr:`FieldDescriptor.camelcase_name`. """ if self._fields_by_camelcase_name is None: self._fields_by_camelcase_name = dict( (f.camelcase_name, f) for f in self.fields) return self._fields_by_camelcase_name def EnumValueName(self, enum, value): """Returns the string name of an enum value. This is just a small helper method to simplify a common operation. Args: enum: string name of the Enum. value: int, value of the enum. Returns: string name of the enum value. Raises: KeyError if either the Enum doesn't exist or the value is not a valid value for the enum. """ return self.enum_types_by_name[enum].values_by_number[value].name def CopyToProto(self, proto): """Copies this to a descriptor_pb2.DescriptorProto. Args: proto: An empty descriptor_pb2.DescriptorProto. """ # This function is overridden to give a better doc comment. super(Descriptor, self).CopyToProto(proto) # TODO(robinson): We should have aggressive checking here, # for example: # * If you specify a repeated field, you should not be allowed # to specify a default value. # * [Other examples here as needed]. # # TODO(robinson): for this and other *Descriptor classes, we # might also want to lock things down aggressively (e.g., # prevent clients from setting the attributes). Having # stronger invariants here in general will reduce the number # of runtime checks we must do in reflection.py... class FieldDescriptor(DescriptorBase): """Descriptor for a single field in a .proto file. Attributes: name (str): Name of this field, exactly as it appears in .proto. full_name (str): Name of this field, including containing scope. This is particularly relevant for extensions. index (int): Dense, 0-indexed index giving the order that this field textually appears within its message in the .proto file. number (int): Tag number declared for this field in the .proto file. type (int): (One of the TYPE_* constants below) Declared type. cpp_type (int): (One of the CPPTYPE_* constants below) C++ type used to represent this field. label (int): (One of the LABEL_* constants below) Tells whether this field is optional, required, or repeated. has_default_value (bool): True if this field has a default value defined, otherwise false. default_value (Varies): Default value of this field. Only meaningful for non-repeated scalar fields. Repeated fields should always set this to [], and non-repeated composite fields should always set this to None. containing_type (Descriptor): Descriptor of the protocol message type that contains this field. Set by the Descriptor constructor if we're passed into one. Somewhat confusingly, for extension fields, this is the descriptor of the EXTENDED message, not the descriptor of the message containing this field. (See is_extension and extension_scope below). message_type (Descriptor): If a composite field, a descriptor of the message type contained in this field. Otherwise, this is None. enum_type (EnumDescriptor): If this field contains an enum, a descriptor of that enum. Otherwise, this is None. is_extension: True iff this describes an extension field. extension_scope (Descriptor): Only meaningful if is_extension is True. Gives the message that immediately contains this extension field. Will be None iff we're a top-level (file-level) extension field. options (descriptor_pb2.FieldOptions): Protocol message field options or None to use default field options. containing_oneof (OneofDescriptor): If the field is a member of a oneof union, contains its descriptor. Otherwise, None. file (FileDescriptor): Reference to file descriptor. """ # Must be consistent with C++ FieldDescriptor::Type enum in # descriptor.h. # # TODO(robinson): Find a way to eliminate this repetition. TYPE_DOUBLE = 1 TYPE_FLOAT = 2 TYPE_INT64 = 3 TYPE_UINT64 = 4 TYPE_INT32 = 5 TYPE_FIXED64 = 6 TYPE_FIXED32 = 7 TYPE_BOOL = 8 TYPE_STRING = 9 TYPE_GROUP = 10 TYPE_MESSAGE = 11 TYPE_BYTES = 12 TYPE_UINT32 = 13 TYPE_ENUM = 14 TYPE_SFIXED32 = 15 TYPE_SFIXED64 = 16 TYPE_SINT32 = 17 TYPE_SINT64 = 18 MAX_TYPE = 18 # Must be consistent with C++ FieldDescriptor::CppType enum in # descriptor.h. # # TODO(robinson): Find a way to eliminate this repetition. CPPTYPE_INT32 = 1 CPPTYPE_INT64 = 2 CPPTYPE_UINT32 = 3 CPPTYPE_UINT64 = 4 CPPTYPE_DOUBLE = 5 CPPTYPE_FLOAT = 6 CPPTYPE_BOOL = 7 CPPTYPE_ENUM = 8 CPPTYPE_STRING = 9 CPPTYPE_MESSAGE = 10 MAX_CPPTYPE = 10 _PYTHON_TO_CPP_PROTO_TYPE_MAP = { TYPE_DOUBLE: CPPTYPE_DOUBLE, TYPE_FLOAT: CPPTYPE_FLOAT, TYPE_ENUM: CPPTYPE_ENUM, TYPE_INT64: CPPTYPE_INT64, TYPE_SINT64: CPPTYPE_INT64, TYPE_SFIXED64: CPPTYPE_INT64, TYPE_UINT64: CPPTYPE_UINT64, TYPE_FIXED64: CPPTYPE_UINT64, TYPE_INT32: CPPTYPE_INT32, TYPE_SFIXED32: CPPTYPE_INT32, TYPE_SINT32: CPPTYPE_INT32, TYPE_UINT32: CPPTYPE_UINT32, TYPE_FIXED32: CPPTYPE_UINT32, TYPE_BYTES: CPPTYPE_STRING, TYPE_STRING: CPPTYPE_STRING, TYPE_BOOL: CPPTYPE_BOOL, TYPE_MESSAGE: CPPTYPE_MESSAGE, TYPE_GROUP: CPPTYPE_MESSAGE } # Must be consistent with C++ FieldDescriptor::Label enum in # descriptor.h. # # TODO(robinson): Find a way to eliminate this repetition. LABEL_OPTIONAL = 1 LABEL_REQUIRED = 2 LABEL_REPEATED = 3 MAX_LABEL = 3 # Must be consistent with C++ constants kMaxNumber, kFirstReservedNumber, # and kLastReservedNumber in descriptor.h MAX_FIELD_NUMBER = (1 << 29) - 1 FIRST_RESERVED_FIELD_NUMBER = 19000 LAST_RESERVED_FIELD_NUMBER = 19999 if _USE_C_DESCRIPTORS: _C_DESCRIPTOR_CLASS = _message.FieldDescriptor def __new__(cls, name, full_name, index, number, type, cpp_type, label, default_value, message_type, enum_type, containing_type, is_extension, extension_scope, options=None, serialized_options=None, has_default_value=True, containing_oneof=None, json_name=None, file=None, create_key=None): # pylint: disable=redefined-builtin _message.Message._CheckCalledFromGeneratedFile() if is_extension: return _message.default_pool.FindExtensionByName(full_name) else: return _message.default_pool.FindFieldByName(full_name) def __init__(self, name, full_name, index, number, type, cpp_type, label, default_value, message_type, enum_type, containing_type, is_extension, extension_scope, options=None, serialized_options=None, has_default_value=True, containing_oneof=None, json_name=None, file=None, create_key=None): # pylint: disable=redefined-builtin """The arguments are as described in the description of FieldDescriptor attributes above. Note that containing_type may be None, and may be set later if necessary (to deal with circular references between message types, for example). Likewise for extension_scope. """ if create_key is not _internal_create_key: _Deprecated('FieldDescriptor') super(FieldDescriptor, self).__init__( options, serialized_options, 'FieldOptions') self.name = name self.full_name = full_name self.file = file self._camelcase_name = None if json_name is None: self.json_name = _ToJsonName(name) else: self.json_name = json_name self.index = index self.number = number self.type = type self.cpp_type = cpp_type self.label = label self.has_default_value = has_default_value self.default_value = default_value self.containing_type = containing_type self.message_type = message_type self.enum_type = enum_type self.is_extension = is_extension self.extension_scope = extension_scope self.containing_oneof = containing_oneof if api_implementation.Type() == 'cpp': if is_extension: self._cdescriptor = _message.default_pool.FindExtensionByName(full_name) else: self._cdescriptor = _message.default_pool.FindFieldByName(full_name) else: self._cdescriptor = None @property def camelcase_name(self): """Camelcase name of this field. Returns: str: the name in CamelCase. """ if self._camelcase_name is None: self._camelcase_name = _ToCamelCase(self.name) return self._camelcase_name @property def has_presence(self): """Whether the field distinguishes between unpopulated and default values. Raises: RuntimeError: singular field that is not linked with message nor file. """ if self.label == FieldDescriptor.LABEL_REPEATED: return False if (self.cpp_type == FieldDescriptor.CPPTYPE_MESSAGE or self.containing_oneof): return True if hasattr(self.file, 'syntax'): return self.file.syntax == 'proto2' if hasattr(self.message_type, 'syntax'): return self.message_type.syntax == 'proto2' raise RuntimeError( 'has_presence is not ready to use because field %s is not' ' linked with message type nor file' % self.full_name) @staticmethod def ProtoTypeToCppProtoType(proto_type): """Converts from a Python proto type to a C++ Proto Type. The Python ProtocolBuffer classes specify both the 'Python' datatype and the 'C++' datatype - and they're not the same. This helper method should translate from one to another. Args: proto_type: the Python proto type (descriptor.FieldDescriptor.TYPE_*) Returns: int: descriptor.FieldDescriptor.CPPTYPE_*, the C++ type. Raises: TypeTransformationError: when the Python proto type isn't known. """ try: return FieldDescriptor._PYTHON_TO_CPP_PROTO_TYPE_MAP[proto_type] except KeyError: raise TypeTransformationError('Unknown proto_type: %s' % proto_type) class EnumDescriptor(_NestedDescriptorBase): """Descriptor for an enum defined in a .proto file. Attributes: name (str): Name of the enum type. full_name (str): Full name of the type, including package name and any enclosing type(s). values (list[EnumValueDescriptor]): List of the values in this enum. values_by_name (dict(str, EnumValueDescriptor)): Same as :attr:`values`, but indexed by the "name" field of each EnumValueDescriptor. values_by_number (dict(int, EnumValueDescriptor)): Same as :attr:`values`, but indexed by the "number" field of each EnumValueDescriptor. containing_type (Descriptor): Descriptor of the immediate containing type of this enum, or None if this is an enum defined at the top level in a .proto file. Set by Descriptor's constructor if we're passed into one. file (FileDescriptor): Reference to file descriptor. options (descriptor_pb2.EnumOptions): Enum options message or None to use default enum options. """ if _USE_C_DESCRIPTORS: _C_DESCRIPTOR_CLASS = _message.EnumDescriptor def __new__(cls, name, full_name, filename, values, containing_type=None, options=None, serialized_options=None, file=None, # pylint: disable=redefined-builtin serialized_start=None, serialized_end=None, create_key=None): _message.Message._CheckCalledFromGeneratedFile() return _message.default_pool.FindEnumTypeByName(full_name) def __init__(self, name, full_name, filename, values, containing_type=None, options=None, serialized_options=None, file=None, # pylint: disable=redefined-builtin serialized_start=None, serialized_end=None, create_key=None): """Arguments are as described in the attribute description above. Note that filename is an obsolete argument, that is not used anymore. Please use file.name to access this as an attribute. """ if create_key is not _internal_create_key: _Deprecated('EnumDescriptor') super(EnumDescriptor, self).__init__( options, 'EnumOptions', name, full_name, file, containing_type, serialized_start=serialized_start, serialized_end=serialized_end, serialized_options=serialized_options) self.values = values for value in self.values: value.type = self self.values_by_name = dict((v.name, v) for v in values) # Values are reversed to ensure that the first alias is retained. self.values_by_number = dict((v.number, v) for v in reversed(values)) def CopyToProto(self, proto): """Copies this to a descriptor_pb2.EnumDescriptorProto. Args: proto (descriptor_pb2.EnumDescriptorProto): An empty descriptor proto. """ # This function is overridden to give a better doc comment. super(EnumDescriptor, self).CopyToProto(proto) class EnumValueDescriptor(DescriptorBase): """Descriptor for a single value within an enum. Attributes: name (str): Name of this value. index (int): Dense, 0-indexed index giving the order that this value appears textually within its enum in the .proto file. number (int): Actual number assigned to this enum value. type (EnumDescriptor): :class:`EnumDescriptor` to which this value belongs. Set by :class:`EnumDescriptor`'s constructor if we're passed into one. options (descriptor_pb2.EnumValueOptions): Enum value options message or None to use default enum value options options. """ if _USE_C_DESCRIPTORS: _C_DESCRIPTOR_CLASS = _message.EnumValueDescriptor def __new__(cls, name, index, number, type=None, # pylint: disable=redefined-builtin options=None, serialized_options=None, create_key=None): _message.Message._CheckCalledFromGeneratedFile() # There is no way we can build a complete EnumValueDescriptor with the # given parameters (the name of the Enum is not known, for example). # Fortunately generated files just pass it to the EnumDescriptor() # constructor, which will ignore it, so returning None is good enough. return None def __init__(self, name, index, number, type=None, # pylint: disable=redefined-builtin options=None, serialized_options=None, create_key=None): """Arguments are as described in the attribute description above.""" if create_key is not _internal_create_key: _Deprecated('EnumValueDescriptor') super(EnumValueDescriptor, self).__init__( options, serialized_options, 'EnumValueOptions') self.name = name self.index = index self.number = number self.type = type class OneofDescriptor(DescriptorBase): """Descriptor for a oneof field. Attributes: name (str): Name of the oneof field. full_name (str): Full name of the oneof field, including package name. index (int): 0-based index giving the order of the oneof field inside its containing type. containing_type (Descriptor): :class:`Descriptor` of the protocol message type that contains this field. Set by the :class:`Descriptor` constructor if we're passed into one. fields (list[FieldDescriptor]): The list of field descriptors this oneof can contain. """ if _USE_C_DESCRIPTORS: _C_DESCRIPTOR_CLASS = _message.OneofDescriptor def __new__( cls, name, full_name, index, containing_type, fields, options=None, serialized_options=None, create_key=None): _message.Message._CheckCalledFromGeneratedFile() return _message.default_pool.FindOneofByName(full_name) def __init__( self, name, full_name, index, containing_type, fields, options=None, serialized_options=None, create_key=None): """Arguments are as described in the attribute description above.""" if create_key is not _internal_create_key: _Deprecated('OneofDescriptor') super(OneofDescriptor, self).__init__( options, serialized_options, 'OneofOptions') self.name = name self.full_name = full_name self.index = index self.containing_type = containing_type self.fields = fields class ServiceDescriptor(_NestedDescriptorBase): """Descriptor for a service. Attributes: name (str): Name of the service. full_name (str): Full name of the service, including package name. index (int): 0-indexed index giving the order that this services definition appears within the .proto file. methods (list[MethodDescriptor]): List of methods provided by this service. methods_by_name (dict(str, MethodDescriptor)): Same :class:`MethodDescriptor` objects as in :attr:`methods_by_name`, but indexed by "name" attribute in each :class:`MethodDescriptor`. options (descriptor_pb2.ServiceOptions): Service options message or None to use default service options. file (FileDescriptor): Reference to file info. """ if _USE_C_DESCRIPTORS: _C_DESCRIPTOR_CLASS = _message.ServiceDescriptor def __new__( cls, name=None, full_name=None, index=None, methods=None, options=None, serialized_options=None, file=None, # pylint: disable=redefined-builtin serialized_start=None, serialized_end=None, create_key=None): _message.Message._CheckCalledFromGeneratedFile() # pylint: disable=protected-access return _message.default_pool.FindServiceByName(full_name) def __init__(self, name, full_name, index, methods, options=None, serialized_options=None, file=None, # pylint: disable=redefined-builtin serialized_start=None, serialized_end=None, create_key=None): if create_key is not _internal_create_key: _Deprecated('ServiceDescriptor') super(ServiceDescriptor, self).__init__( options, 'ServiceOptions', name, full_name, file, None, serialized_start=serialized_start, serialized_end=serialized_end, serialized_options=serialized_options) self.index = index self.methods = methods self.methods_by_name = dict((m.name, m) for m in methods) # Set the containing service for each method in this service. for method in self.methods: method.containing_service = self def FindMethodByName(self, name): """Searches for the specified method, and returns its descriptor. Args: name (str): Name of the method. Returns: MethodDescriptor or None: the descriptor for the requested method, if found. """ return self.methods_by_name.get(name, None) def CopyToProto(self, proto): """Copies this to a descriptor_pb2.ServiceDescriptorProto. Args: proto (descriptor_pb2.ServiceDescriptorProto): An empty descriptor proto. """ # This function is overridden to give a better doc comment. super(ServiceDescriptor, self).CopyToProto(proto) class MethodDescriptor(DescriptorBase): """Descriptor for a method in a service. Attributes: name (str): Name of the method within the service. full_name (str): Full name of method. index (int): 0-indexed index of the method inside the service. containing_service (ServiceDescriptor): The service that contains this method. input_type (Descriptor): The descriptor of the message that this method accepts. output_type (Descriptor): The descriptor of the message that this method returns. client_streaming (bool): Whether this method uses client streaming. server_streaming (bool): Whether this method uses server streaming. options (descriptor_pb2.MethodOptions or None): Method options message, or None to use default method options. """ if _USE_C_DESCRIPTORS: _C_DESCRIPTOR_CLASS = _message.MethodDescriptor def __new__(cls, name, full_name, index, containing_service, input_type, output_type, client_streaming=False, server_streaming=False, options=None, serialized_options=None, create_key=None): _message.Message._CheckCalledFromGeneratedFile() # pylint: disable=protected-access return _message.default_pool.FindMethodByName(full_name) def __init__(self, name, full_name, index, containing_service, input_type, output_type, client_streaming=False, server_streaming=False, options=None, serialized_options=None, create_key=None): """The arguments are as described in the description of MethodDescriptor attributes above. Note that containing_service may be None, and may be set later if necessary. """ if create_key is not _internal_create_key: _Deprecated('MethodDescriptor') super(MethodDescriptor, self).__init__( options, serialized_options, 'MethodOptions') self.name = name self.full_name = full_name self.index = index self.containing_service = containing_service self.input_type = input_type self.output_type = output_type self.client_streaming = client_streaming self.server_streaming = server_streaming def CopyToProto(self, proto): """Copies this to a descriptor_pb2.MethodDescriptorProto. Args: proto (descriptor_pb2.MethodDescriptorProto): An empty descriptor proto. Raises: Error: If self couldn't be serialized, due to too few constructor arguments. """ if self.containing_service is not None: from google.protobuf import descriptor_pb2 service_proto = descriptor_pb2.ServiceDescriptorProto() self.containing_service.CopyToProto(service_proto) proto.CopyFrom(service_proto.method[self.index]) else: raise Error('Descriptor does not contain a service.') class FileDescriptor(DescriptorBase): """Descriptor for a file. Mimics the descriptor_pb2.FileDescriptorProto. Note that :attr:`enum_types_by_name`, :attr:`extensions_by_name`, and :attr:`dependencies` fields are only set by the :py:mod:`google.protobuf.message_factory` module, and not by the generated proto code. Attributes: name (str): Name of file, relative to root of source tree. package (str): Name of the package syntax (str): string indicating syntax of the file (can be "proto2" or "proto3") serialized_pb (bytes): Byte string of serialized :class:`descriptor_pb2.FileDescriptorProto`. dependencies (list[FileDescriptor]): List of other :class:`FileDescriptor` objects this :class:`FileDescriptor` depends on. public_dependencies (list[FileDescriptor]): A subset of :attr:`dependencies`, which were declared as "public". message_types_by_name (dict(str, Descriptor)): Mapping from message names to their :class:`Descriptor`. enum_types_by_name (dict(str, EnumDescriptor)): Mapping from enum names to their :class:`EnumDescriptor`. extensions_by_name (dict(str, FieldDescriptor)): Mapping from extension names declared at file scope to their :class:`FieldDescriptor`. services_by_name (dict(str, ServiceDescriptor)): Mapping from services' names to their :class:`ServiceDescriptor`. pool (DescriptorPool): The pool this descriptor belongs to. When not passed to the constructor, the global default pool is used. """ if _USE_C_DESCRIPTORS: _C_DESCRIPTOR_CLASS = _message.FileDescriptor def __new__(cls, name, package, options=None, serialized_options=None, serialized_pb=None, dependencies=None, public_dependencies=None, syntax=None, pool=None, create_key=None): # FileDescriptor() is called from various places, not only from generated # files, to register dynamic proto files and messages. # pylint: disable=g-explicit-bool-comparison if serialized_pb == b'': # Cpp generated code must be linked in if serialized_pb is '' try: return _message.default_pool.FindFileByName(name) except KeyError: raise RuntimeError('Please link in cpp generated lib for %s' % (name)) elif serialized_pb: return _message.default_pool.AddSerializedFile(serialized_pb) else: return super(FileDescriptor, cls).__new__(cls) def __init__(self, name, package, options=None, serialized_options=None, serialized_pb=None, dependencies=None, public_dependencies=None, syntax=None, pool=None, create_key=None): """Constructor.""" if create_key is not _internal_create_key: _Deprecated('FileDescriptor') super(FileDescriptor, self).__init__( options, serialized_options, 'FileOptions') if pool is None: from google.protobuf import descriptor_pool pool = descriptor_pool.Default() self.pool = pool self.message_types_by_name = {} self.name = name self.package = package self.syntax = syntax or "proto2" self.serialized_pb = serialized_pb self.enum_types_by_name = {} self.extensions_by_name = {} self.services_by_name = {} self.dependencies = (dependencies or []) self.public_dependencies = (public_dependencies or []) def CopyToProto(self, proto): """Copies this to a descriptor_pb2.FileDescriptorProto. Args: proto: An empty descriptor_pb2.FileDescriptorProto. """ proto.ParseFromString(self.serialized_pb) def _ParseOptions(message, string): """Parses serialized options. This helper function is used to parse serialized options in generated proto2 files. It must not be used outside proto2. """ message.ParseFromString(string) return message def _ToCamelCase(name): """Converts name to camel-case and returns it.""" capitalize_next = False result = [] for c in name: if c == '_': if result: capitalize_next = True elif capitalize_next: result.append(c.upper()) capitalize_next = False else: result += c # Lower-case the first letter. if result and result[0].isupper(): result[0] = result[0].lower() return ''.join(result) def _OptionsOrNone(descriptor_proto): """Returns the value of the field `options`, or None if it is not set.""" if descriptor_proto.HasField('options'): return descriptor_proto.options else: return None def _ToJsonName(name): """Converts name to Json name and returns it.""" capitalize_next = False result = [] for c in name: if c == '_': capitalize_next = True elif capitalize_next: result.append(c.upper()) capitalize_next = False else: result += c return ''.join(result) def MakeDescriptor(desc_proto, package='', build_file_if_cpp=True, syntax=None): """Make a protobuf Descriptor given a DescriptorProto protobuf. Handles nested descriptors. Note that this is limited to the scope of defining a message inside of another message. Composite fields can currently only be resolved if the message is defined in the same scope as the field. Args: desc_proto: The descriptor_pb2.DescriptorProto protobuf message. package: Optional package name for the new message Descriptor (string). build_file_if_cpp: Update the C++ descriptor pool if api matches. Set to False on recursion, so no duplicates are created. syntax: The syntax/semantics that should be used. Set to "proto3" to get proto3 field presence semantics. Returns: A Descriptor for protobuf messages. """ if api_implementation.Type() == 'cpp' and build_file_if_cpp: # The C++ implementation requires all descriptors to be backed by the same # definition in the C++ descriptor pool. To do this, we build a # FileDescriptorProto with the same definition as this descriptor and build # it into the pool. from google.protobuf import descriptor_pb2 file_descriptor_proto = descriptor_pb2.FileDescriptorProto() file_descriptor_proto.message_type.add().MergeFrom(desc_proto) # Generate a random name for this proto file to prevent conflicts with any # imported ones. We need to specify a file name so the descriptor pool # accepts our FileDescriptorProto, but it is not important what that file # name is actually set to. proto_name = binascii.hexlify(os.urandom(16)).decode('ascii') if package: file_descriptor_proto.name = os.path.join(package.replace('.', '/'), proto_name + '.proto') file_descriptor_proto.package = package else: file_descriptor_proto.name = proto_name + '.proto' _message.default_pool.Add(file_descriptor_proto) result = _message.default_pool.FindFileByName(file_descriptor_proto.name) if _USE_C_DESCRIPTORS: return result.message_types_by_name[desc_proto.name] full_message_name = [desc_proto.name] if package: full_message_name.insert(0, package) # Create Descriptors for enum types enum_types = {} for enum_proto in desc_proto.enum_type: full_name = '.'.join(full_message_name + [enum_proto.name]) enum_desc = EnumDescriptor( enum_proto.name, full_name, None, [ EnumValueDescriptor(enum_val.name, ii, enum_val.number, create_key=_internal_create_key) for ii, enum_val in enumerate(enum_proto.value)], create_key=_internal_create_key) enum_types[full_name] = enum_desc # Create Descriptors for nested types nested_types = {} for nested_proto in desc_proto.nested_type: full_name = '.'.join(full_message_name + [nested_proto.name]) # Nested types are just those defined inside of the message, not all types # used by fields in the message, so no loops are possible here. nested_desc = MakeDescriptor(nested_proto, package='.'.join(full_message_name), build_file_if_cpp=False, syntax=syntax) nested_types[full_name] = nested_desc fields = [] for field_proto in desc_proto.field: full_name = '.'.join(full_message_name + [field_proto.name]) enum_desc = None nested_desc = None if field_proto.json_name: json_name = field_proto.json_name else: json_name = None if field_proto.HasField('type_name'): type_name = field_proto.type_name full_type_name = '.'.join(full_message_name + [type_name[type_name.rfind('.')+1:]]) if full_type_name in nested_types: nested_desc = nested_types[full_type_name] elif full_type_name in enum_types: enum_desc = enum_types[full_type_name] # Else type_name references a non-local type, which isn't implemented field = FieldDescriptor( field_proto.name, full_name, field_proto.number - 1, field_proto.number, field_proto.type, FieldDescriptor.ProtoTypeToCppProtoType(field_proto.type), field_proto.label, None, nested_desc, enum_desc, None, False, None, options=_OptionsOrNone(field_proto), has_default_value=False, json_name=json_name, create_key=_internal_create_key) fields.append(field) desc_name = '.'.join(full_message_name) return Descriptor(desc_proto.name, desc_name, None, None, fields, list(nested_types.values()), list(enum_types.values()), [], options=_OptionsOrNone(desc_proto), create_key=_internal_create_key) ================================================ FILE: openpype/hosts/nuke/vendor/google/protobuf/descriptor_database.py ================================================ # Protocol Buffers - Google's data interchange format # Copyright 2008 Google Inc. All rights reserved. # https://developers.google.com/protocol-buffers/ # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are # met: # # * Redistributions of source code must retain the above copyright # notice, this list of conditions and the following disclaimer. # * Redistributions in binary form must reproduce the above # copyright notice, this list of conditions and the following disclaimer # in the documentation and/or other materials provided with the # distribution. # * Neither the name of Google Inc. nor the names of its # contributors may be used to endorse or promote products derived from # this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. """Provides a container for DescriptorProtos.""" __author__ = 'matthewtoia@google.com (Matt Toia)' import warnings class Error(Exception): pass class DescriptorDatabaseConflictingDefinitionError(Error): """Raised when a proto is added with the same name & different descriptor.""" class DescriptorDatabase(object): """A container accepting FileDescriptorProtos and maps DescriptorProtos.""" def __init__(self): self._file_desc_protos_by_file = {} self._file_desc_protos_by_symbol = {} def Add(self, file_desc_proto): """Adds the FileDescriptorProto and its types to this database. Args: file_desc_proto: The FileDescriptorProto to add. Raises: DescriptorDatabaseConflictingDefinitionError: if an attempt is made to add a proto with the same name but different definition than an existing proto in the database. """ proto_name = file_desc_proto.name if proto_name not in self._file_desc_protos_by_file: self._file_desc_protos_by_file[proto_name] = file_desc_proto elif self._file_desc_protos_by_file[proto_name] != file_desc_proto: raise DescriptorDatabaseConflictingDefinitionError( '%s already added, but with different descriptor.' % proto_name) else: return # Add all the top-level descriptors to the index. package = file_desc_proto.package for message in file_desc_proto.message_type: for name in _ExtractSymbols(message, package): self._AddSymbol(name, file_desc_proto) for enum in file_desc_proto.enum_type: self._AddSymbol(('.'.join((package, enum.name))), file_desc_proto) for enum_value in enum.value: self._file_desc_protos_by_symbol[ '.'.join((package, enum_value.name))] = file_desc_proto for extension in file_desc_proto.extension: self._AddSymbol(('.'.join((package, extension.name))), file_desc_proto) for service in file_desc_proto.service: self._AddSymbol(('.'.join((package, service.name))), file_desc_proto) def FindFileByName(self, name): """Finds the file descriptor proto by file name. Typically the file name is a relative path ending to a .proto file. The proto with the given name will have to have been added to this database using the Add method or else an error will be raised. Args: name: The file name to find. Returns: The file descriptor proto matching the name. Raises: KeyError if no file by the given name was added. """ return self._file_desc_protos_by_file[name] def FindFileContainingSymbol(self, symbol): """Finds the file descriptor proto containing the specified symbol. The symbol should be a fully qualified name including the file descriptor's package and any containing messages. Some examples: 'some.package.name.Message' 'some.package.name.Message.NestedEnum' 'some.package.name.Message.some_field' The file descriptor proto containing the specified symbol must be added to this database using the Add method or else an error will be raised. Args: symbol: The fully qualified symbol name. Returns: The file descriptor proto containing the symbol. Raises: KeyError if no file contains the specified symbol. """ try: return self._file_desc_protos_by_symbol[symbol] except KeyError: # Fields, enum values, and nested extensions are not in # _file_desc_protos_by_symbol. Try to find the top level # descriptor. Non-existent nested symbol under a valid top level # descriptor can also be found. The behavior is the same with # protobuf C++. top_level, _, _ = symbol.rpartition('.') try: return self._file_desc_protos_by_symbol[top_level] except KeyError: # Raise the original symbol as a KeyError for better diagnostics. raise KeyError(symbol) def FindFileContainingExtension(self, extendee_name, extension_number): # TODO(jieluo): implement this API. return None def FindAllExtensionNumbers(self, extendee_name): # TODO(jieluo): implement this API. return [] def _AddSymbol(self, name, file_desc_proto): if name in self._file_desc_protos_by_symbol: warn_msg = ('Conflict register for file "' + file_desc_proto.name + '": ' + name + ' is already defined in file "' + self._file_desc_protos_by_symbol[name].name + '"') warnings.warn(warn_msg, RuntimeWarning) self._file_desc_protos_by_symbol[name] = file_desc_proto def _ExtractSymbols(desc_proto, package): """Pulls out all the symbols from a descriptor proto. Args: desc_proto: The proto to extract symbols from. package: The package containing the descriptor type. Yields: The fully qualified name found in the descriptor. """ message_name = package + '.' + desc_proto.name if package else desc_proto.name yield message_name for nested_type in desc_proto.nested_type: for symbol in _ExtractSymbols(nested_type, message_name): yield symbol for enum_type in desc_proto.enum_type: yield '.'.join((message_name, enum_type.name)) ================================================ FILE: openpype/hosts/nuke/vendor/google/protobuf/descriptor_pb2.py ================================================ # -*- coding: utf-8 -*- # Generated by the protocol buffer compiler. DO NOT EDIT! # source: google/protobuf/descriptor.proto """Generated protocol buffer code.""" from google.protobuf.internal import builder as _builder from google.protobuf import descriptor as _descriptor from google.protobuf import descriptor_pool as _descriptor_pool from google.protobuf import symbol_database as _symbol_database # @@protoc_insertion_point(imports) _sym_db = _symbol_database.Default() if _descriptor._USE_C_DESCRIPTORS == False: DESCRIPTOR = _descriptor.FileDescriptor( name='google/protobuf/descriptor.proto', package='google.protobuf', syntax='proto2', serialized_options=None, create_key=_descriptor._internal_create_key, serialized_pb=b'\n google/protobuf/descriptor.proto\x12\x0fgoogle.protobuf\"G\n\x11\x46ileDescriptorSet\x12\x32\n\x04\x66ile\x18\x01 \x03(\x0b\x32$.google.protobuf.FileDescriptorProto\"\xdb\x03\n\x13\x46ileDescriptorProto\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0f\n\x07package\x18\x02 \x01(\t\x12\x12\n\ndependency\x18\x03 \x03(\t\x12\x19\n\x11public_dependency\x18\n \x03(\x05\x12\x17\n\x0fweak_dependency\x18\x0b \x03(\x05\x12\x36\n\x0cmessage_type\x18\x04 \x03(\x0b\x32 .google.protobuf.DescriptorProto\x12\x37\n\tenum_type\x18\x05 \x03(\x0b\x32$.google.protobuf.EnumDescriptorProto\x12\x38\n\x07service\x18\x06 \x03(\x0b\x32\'.google.protobuf.ServiceDescriptorProto\x12\x38\n\textension\x18\x07 \x03(\x0b\x32%.google.protobuf.FieldDescriptorProto\x12-\n\x07options\x18\x08 \x01(\x0b\x32\x1c.google.protobuf.FileOptions\x12\x39\n\x10source_code_info\x18\t \x01(\x0b\x32\x1f.google.protobuf.SourceCodeInfo\x12\x0e\n\x06syntax\x18\x0c \x01(\t\"\xa9\x05\n\x0f\x44\x65scriptorProto\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x34\n\x05\x66ield\x18\x02 \x03(\x0b\x32%.google.protobuf.FieldDescriptorProto\x12\x38\n\textension\x18\x06 \x03(\x0b\x32%.google.protobuf.FieldDescriptorProto\x12\x35\n\x0bnested_type\x18\x03 \x03(\x0b\x32 .google.protobuf.DescriptorProto\x12\x37\n\tenum_type\x18\x04 \x03(\x0b\x32$.google.protobuf.EnumDescriptorProto\x12H\n\x0f\x65xtension_range\x18\x05 \x03(\x0b\x32/.google.protobuf.DescriptorProto.ExtensionRange\x12\x39\n\noneof_decl\x18\x08 \x03(\x0b\x32%.google.protobuf.OneofDescriptorProto\x12\x30\n\x07options\x18\x07 \x01(\x0b\x32\x1f.google.protobuf.MessageOptions\x12\x46\n\x0ereserved_range\x18\t \x03(\x0b\x32..google.protobuf.DescriptorProto.ReservedRange\x12\x15\n\rreserved_name\x18\n \x03(\t\x1a\x65\n\x0e\x45xtensionRange\x12\r\n\x05start\x18\x01 \x01(\x05\x12\x0b\n\x03\x65nd\x18\x02 \x01(\x05\x12\x37\n\x07options\x18\x03 \x01(\x0b\x32&.google.protobuf.ExtensionRangeOptions\x1a+\n\rReservedRange\x12\r\n\x05start\x18\x01 \x01(\x05\x12\x0b\n\x03\x65nd\x18\x02 \x01(\x05\"g\n\x15\x45xtensionRangeOptions\x12\x43\n\x14uninterpreted_option\x18\xe7\x07 \x03(\x0b\x32$.google.protobuf.UninterpretedOption*\t\x08\xe8\x07\x10\x80\x80\x80\x80\x02\"\xd5\x05\n\x14\x46ieldDescriptorProto\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0e\n\x06number\x18\x03 \x01(\x05\x12:\n\x05label\x18\x04 \x01(\x0e\x32+.google.protobuf.FieldDescriptorProto.Label\x12\x38\n\x04type\x18\x05 \x01(\x0e\x32*.google.protobuf.FieldDescriptorProto.Type\x12\x11\n\ttype_name\x18\x06 \x01(\t\x12\x10\n\x08\x65xtendee\x18\x02 \x01(\t\x12\x15\n\rdefault_value\x18\x07 \x01(\t\x12\x13\n\x0boneof_index\x18\t \x01(\x05\x12\x11\n\tjson_name\x18\n \x01(\t\x12.\n\x07options\x18\x08 \x01(\x0b\x32\x1d.google.protobuf.FieldOptions\x12\x17\n\x0fproto3_optional\x18\x11 \x01(\x08\"\xb6\x02\n\x04Type\x12\x0f\n\x0bTYPE_DOUBLE\x10\x01\x12\x0e\n\nTYPE_FLOAT\x10\x02\x12\x0e\n\nTYPE_INT64\x10\x03\x12\x0f\n\x0bTYPE_UINT64\x10\x04\x12\x0e\n\nTYPE_INT32\x10\x05\x12\x10\n\x0cTYPE_FIXED64\x10\x06\x12\x10\n\x0cTYPE_FIXED32\x10\x07\x12\r\n\tTYPE_BOOL\x10\x08\x12\x0f\n\x0bTYPE_STRING\x10\t\x12\x0e\n\nTYPE_GROUP\x10\n\x12\x10\n\x0cTYPE_MESSAGE\x10\x0b\x12\x0e\n\nTYPE_BYTES\x10\x0c\x12\x0f\n\x0bTYPE_UINT32\x10\r\x12\r\n\tTYPE_ENUM\x10\x0e\x12\x11\n\rTYPE_SFIXED32\x10\x0f\x12\x11\n\rTYPE_SFIXED64\x10\x10\x12\x0f\n\x0bTYPE_SINT32\x10\x11\x12\x0f\n\x0bTYPE_SINT64\x10\x12\"C\n\x05Label\x12\x12\n\x0eLABEL_OPTIONAL\x10\x01\x12\x12\n\x0eLABEL_REQUIRED\x10\x02\x12\x12\n\x0eLABEL_REPEATED\x10\x03\"T\n\x14OneofDescriptorProto\x12\x0c\n\x04name\x18\x01 \x01(\t\x12.\n\x07options\x18\x02 \x01(\x0b\x32\x1d.google.protobuf.OneofOptions\"\xa4\x02\n\x13\x45numDescriptorProto\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x38\n\x05value\x18\x02 \x03(\x0b\x32).google.protobuf.EnumValueDescriptorProto\x12-\n\x07options\x18\x03 \x01(\x0b\x32\x1c.google.protobuf.EnumOptions\x12N\n\x0ereserved_range\x18\x04 \x03(\x0b\x32\x36.google.protobuf.EnumDescriptorProto.EnumReservedRange\x12\x15\n\rreserved_name\x18\x05 \x03(\t\x1a/\n\x11\x45numReservedRange\x12\r\n\x05start\x18\x01 \x01(\x05\x12\x0b\n\x03\x65nd\x18\x02 \x01(\x05\"l\n\x18\x45numValueDescriptorProto\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0e\n\x06number\x18\x02 \x01(\x05\x12\x32\n\x07options\x18\x03 \x01(\x0b\x32!.google.protobuf.EnumValueOptions\"\x90\x01\n\x16ServiceDescriptorProto\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x36\n\x06method\x18\x02 \x03(\x0b\x32&.google.protobuf.MethodDescriptorProto\x12\x30\n\x07options\x18\x03 \x01(\x0b\x32\x1f.google.protobuf.ServiceOptions\"\xc1\x01\n\x15MethodDescriptorProto\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x12\n\ninput_type\x18\x02 \x01(\t\x12\x13\n\x0boutput_type\x18\x03 \x01(\t\x12/\n\x07options\x18\x04 \x01(\x0b\x32\x1e.google.protobuf.MethodOptions\x12\x1f\n\x10\x63lient_streaming\x18\x05 \x01(\x08:\x05\x66\x61lse\x12\x1f\n\x10server_streaming\x18\x06 \x01(\x08:\x05\x66\x61lse\"\xa5\x06\n\x0b\x46ileOptions\x12\x14\n\x0cjava_package\x18\x01 \x01(\t\x12\x1c\n\x14java_outer_classname\x18\x08 \x01(\t\x12\"\n\x13java_multiple_files\x18\n \x01(\x08:\x05\x66\x61lse\x12)\n\x1djava_generate_equals_and_hash\x18\x14 \x01(\x08\x42\x02\x18\x01\x12%\n\x16java_string_check_utf8\x18\x1b \x01(\x08:\x05\x66\x61lse\x12\x46\n\x0coptimize_for\x18\t \x01(\x0e\x32).google.protobuf.FileOptions.OptimizeMode:\x05SPEED\x12\x12\n\ngo_package\x18\x0b \x01(\t\x12\"\n\x13\x63\x63_generic_services\x18\x10 \x01(\x08:\x05\x66\x61lse\x12$\n\x15java_generic_services\x18\x11 \x01(\x08:\x05\x66\x61lse\x12\"\n\x13py_generic_services\x18\x12 \x01(\x08:\x05\x66\x61lse\x12#\n\x14php_generic_services\x18* \x01(\x08:\x05\x66\x61lse\x12\x19\n\ndeprecated\x18\x17 \x01(\x08:\x05\x66\x61lse\x12\x1e\n\x10\x63\x63_enable_arenas\x18\x1f \x01(\x08:\x04true\x12\x19\n\x11objc_class_prefix\x18$ \x01(\t\x12\x18\n\x10\x63sharp_namespace\x18% \x01(\t\x12\x14\n\x0cswift_prefix\x18\' \x01(\t\x12\x18\n\x10php_class_prefix\x18( \x01(\t\x12\x15\n\rphp_namespace\x18) \x01(\t\x12\x1e\n\x16php_metadata_namespace\x18, \x01(\t\x12\x14\n\x0cruby_package\x18- \x01(\t\x12\x43\n\x14uninterpreted_option\x18\xe7\x07 \x03(\x0b\x32$.google.protobuf.UninterpretedOption\":\n\x0cOptimizeMode\x12\t\n\x05SPEED\x10\x01\x12\r\n\tCODE_SIZE\x10\x02\x12\x10\n\x0cLITE_RUNTIME\x10\x03*\t\x08\xe8\x07\x10\x80\x80\x80\x80\x02J\x04\x08&\x10\'\"\x84\x02\n\x0eMessageOptions\x12&\n\x17message_set_wire_format\x18\x01 \x01(\x08:\x05\x66\x61lse\x12.\n\x1fno_standard_descriptor_accessor\x18\x02 \x01(\x08:\x05\x66\x61lse\x12\x19\n\ndeprecated\x18\x03 \x01(\x08:\x05\x66\x61lse\x12\x11\n\tmap_entry\x18\x07 \x01(\x08\x12\x43\n\x14uninterpreted_option\x18\xe7\x07 \x03(\x0b\x32$.google.protobuf.UninterpretedOption*\t\x08\xe8\x07\x10\x80\x80\x80\x80\x02J\x04\x08\x04\x10\x05J\x04\x08\x05\x10\x06J\x04\x08\x06\x10\x07J\x04\x08\x08\x10\tJ\x04\x08\t\x10\n\"\xbe\x03\n\x0c\x46ieldOptions\x12:\n\x05\x63type\x18\x01 \x01(\x0e\x32#.google.protobuf.FieldOptions.CType:\x06STRING\x12\x0e\n\x06packed\x18\x02 \x01(\x08\x12?\n\x06jstype\x18\x06 \x01(\x0e\x32$.google.protobuf.FieldOptions.JSType:\tJS_NORMAL\x12\x13\n\x04lazy\x18\x05 \x01(\x08:\x05\x66\x61lse\x12\x1e\n\x0funverified_lazy\x18\x0f \x01(\x08:\x05\x66\x61lse\x12\x19\n\ndeprecated\x18\x03 \x01(\x08:\x05\x66\x61lse\x12\x13\n\x04weak\x18\n \x01(\x08:\x05\x66\x61lse\x12\x43\n\x14uninterpreted_option\x18\xe7\x07 \x03(\x0b\x32$.google.protobuf.UninterpretedOption\"/\n\x05\x43Type\x12\n\n\x06STRING\x10\x00\x12\x08\n\x04\x43ORD\x10\x01\x12\x10\n\x0cSTRING_PIECE\x10\x02\"5\n\x06JSType\x12\r\n\tJS_NORMAL\x10\x00\x12\r\n\tJS_STRING\x10\x01\x12\r\n\tJS_NUMBER\x10\x02*\t\x08\xe8\x07\x10\x80\x80\x80\x80\x02J\x04\x08\x04\x10\x05\"^\n\x0cOneofOptions\x12\x43\n\x14uninterpreted_option\x18\xe7\x07 \x03(\x0b\x32$.google.protobuf.UninterpretedOption*\t\x08\xe8\x07\x10\x80\x80\x80\x80\x02\"\x93\x01\n\x0b\x45numOptions\x12\x13\n\x0b\x61llow_alias\x18\x02 \x01(\x08\x12\x19\n\ndeprecated\x18\x03 \x01(\x08:\x05\x66\x61lse\x12\x43\n\x14uninterpreted_option\x18\xe7\x07 \x03(\x0b\x32$.google.protobuf.UninterpretedOption*\t\x08\xe8\x07\x10\x80\x80\x80\x80\x02J\x04\x08\x05\x10\x06\"}\n\x10\x45numValueOptions\x12\x19\n\ndeprecated\x18\x01 \x01(\x08:\x05\x66\x61lse\x12\x43\n\x14uninterpreted_option\x18\xe7\x07 \x03(\x0b\x32$.google.protobuf.UninterpretedOption*\t\x08\xe8\x07\x10\x80\x80\x80\x80\x02\"{\n\x0eServiceOptions\x12\x19\n\ndeprecated\x18! \x01(\x08:\x05\x66\x61lse\x12\x43\n\x14uninterpreted_option\x18\xe7\x07 \x03(\x0b\x32$.google.protobuf.UninterpretedOption*\t\x08\xe8\x07\x10\x80\x80\x80\x80\x02\"\xad\x02\n\rMethodOptions\x12\x19\n\ndeprecated\x18! \x01(\x08:\x05\x66\x61lse\x12_\n\x11idempotency_level\x18\" \x01(\x0e\x32/.google.protobuf.MethodOptions.IdempotencyLevel:\x13IDEMPOTENCY_UNKNOWN\x12\x43\n\x14uninterpreted_option\x18\xe7\x07 \x03(\x0b\x32$.google.protobuf.UninterpretedOption\"P\n\x10IdempotencyLevel\x12\x17\n\x13IDEMPOTENCY_UNKNOWN\x10\x00\x12\x13\n\x0fNO_SIDE_EFFECTS\x10\x01\x12\x0e\n\nIDEMPOTENT\x10\x02*\t\x08\xe8\x07\x10\x80\x80\x80\x80\x02\"\x9e\x02\n\x13UninterpretedOption\x12;\n\x04name\x18\x02 \x03(\x0b\x32-.google.protobuf.UninterpretedOption.NamePart\x12\x18\n\x10identifier_value\x18\x03 \x01(\t\x12\x1a\n\x12positive_int_value\x18\x04 \x01(\x04\x12\x1a\n\x12negative_int_value\x18\x05 \x01(\x03\x12\x14\n\x0c\x64ouble_value\x18\x06 \x01(\x01\x12\x14\n\x0cstring_value\x18\x07 \x01(\x0c\x12\x17\n\x0f\x61ggregate_value\x18\x08 \x01(\t\x1a\x33\n\x08NamePart\x12\x11\n\tname_part\x18\x01 \x02(\t\x12\x14\n\x0cis_extension\x18\x02 \x02(\x08\"\xd5\x01\n\x0eSourceCodeInfo\x12:\n\x08location\x18\x01 \x03(\x0b\x32(.google.protobuf.SourceCodeInfo.Location\x1a\x86\x01\n\x08Location\x12\x10\n\x04path\x18\x01 \x03(\x05\x42\x02\x10\x01\x12\x10\n\x04span\x18\x02 \x03(\x05\x42\x02\x10\x01\x12\x18\n\x10leading_comments\x18\x03 \x01(\t\x12\x19\n\x11trailing_comments\x18\x04 \x01(\t\x12!\n\x19leading_detached_comments\x18\x06 \x03(\t\"\xa7\x01\n\x11GeneratedCodeInfo\x12\x41\n\nannotation\x18\x01 \x03(\x0b\x32-.google.protobuf.GeneratedCodeInfo.Annotation\x1aO\n\nAnnotation\x12\x10\n\x04path\x18\x01 \x03(\x05\x42\x02\x10\x01\x12\x13\n\x0bsource_file\x18\x02 \x01(\t\x12\r\n\x05\x62\x65gin\x18\x03 \x01(\x05\x12\x0b\n\x03\x65nd\x18\x04 \x01(\x05\x42~\n\x13\x63om.google.protobufB\x10\x44\x65scriptorProtosH\x01Z-google.golang.org/protobuf/types/descriptorpb\xf8\x01\x01\xa2\x02\x03GPB\xaa\x02\x1aGoogle.Protobuf.Reflection' ) else: DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n google/protobuf/descriptor.proto\x12\x0fgoogle.protobuf\"G\n\x11\x46ileDescriptorSet\x12\x32\n\x04\x66ile\x18\x01 \x03(\x0b\x32$.google.protobuf.FileDescriptorProto\"\xdb\x03\n\x13\x46ileDescriptorProto\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0f\n\x07package\x18\x02 \x01(\t\x12\x12\n\ndependency\x18\x03 \x03(\t\x12\x19\n\x11public_dependency\x18\n \x03(\x05\x12\x17\n\x0fweak_dependency\x18\x0b \x03(\x05\x12\x36\n\x0cmessage_type\x18\x04 \x03(\x0b\x32 .google.protobuf.DescriptorProto\x12\x37\n\tenum_type\x18\x05 \x03(\x0b\x32$.google.protobuf.EnumDescriptorProto\x12\x38\n\x07service\x18\x06 \x03(\x0b\x32\'.google.protobuf.ServiceDescriptorProto\x12\x38\n\textension\x18\x07 \x03(\x0b\x32%.google.protobuf.FieldDescriptorProto\x12-\n\x07options\x18\x08 \x01(\x0b\x32\x1c.google.protobuf.FileOptions\x12\x39\n\x10source_code_info\x18\t \x01(\x0b\x32\x1f.google.protobuf.SourceCodeInfo\x12\x0e\n\x06syntax\x18\x0c \x01(\t\"\xa9\x05\n\x0f\x44\x65scriptorProto\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x34\n\x05\x66ield\x18\x02 \x03(\x0b\x32%.google.protobuf.FieldDescriptorProto\x12\x38\n\textension\x18\x06 \x03(\x0b\x32%.google.protobuf.FieldDescriptorProto\x12\x35\n\x0bnested_type\x18\x03 \x03(\x0b\x32 .google.protobuf.DescriptorProto\x12\x37\n\tenum_type\x18\x04 \x03(\x0b\x32$.google.protobuf.EnumDescriptorProto\x12H\n\x0f\x65xtension_range\x18\x05 \x03(\x0b\x32/.google.protobuf.DescriptorProto.ExtensionRange\x12\x39\n\noneof_decl\x18\x08 \x03(\x0b\x32%.google.protobuf.OneofDescriptorProto\x12\x30\n\x07options\x18\x07 \x01(\x0b\x32\x1f.google.protobuf.MessageOptions\x12\x46\n\x0ereserved_range\x18\t \x03(\x0b\x32..google.protobuf.DescriptorProto.ReservedRange\x12\x15\n\rreserved_name\x18\n \x03(\t\x1a\x65\n\x0e\x45xtensionRange\x12\r\n\x05start\x18\x01 \x01(\x05\x12\x0b\n\x03\x65nd\x18\x02 \x01(\x05\x12\x37\n\x07options\x18\x03 \x01(\x0b\x32&.google.protobuf.ExtensionRangeOptions\x1a+\n\rReservedRange\x12\r\n\x05start\x18\x01 \x01(\x05\x12\x0b\n\x03\x65nd\x18\x02 \x01(\x05\"g\n\x15\x45xtensionRangeOptions\x12\x43\n\x14uninterpreted_option\x18\xe7\x07 \x03(\x0b\x32$.google.protobuf.UninterpretedOption*\t\x08\xe8\x07\x10\x80\x80\x80\x80\x02\"\xd5\x05\n\x14\x46ieldDescriptorProto\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0e\n\x06number\x18\x03 \x01(\x05\x12:\n\x05label\x18\x04 \x01(\x0e\x32+.google.protobuf.FieldDescriptorProto.Label\x12\x38\n\x04type\x18\x05 \x01(\x0e\x32*.google.protobuf.FieldDescriptorProto.Type\x12\x11\n\ttype_name\x18\x06 \x01(\t\x12\x10\n\x08\x65xtendee\x18\x02 \x01(\t\x12\x15\n\rdefault_value\x18\x07 \x01(\t\x12\x13\n\x0boneof_index\x18\t \x01(\x05\x12\x11\n\tjson_name\x18\n \x01(\t\x12.\n\x07options\x18\x08 \x01(\x0b\x32\x1d.google.protobuf.FieldOptions\x12\x17\n\x0fproto3_optional\x18\x11 \x01(\x08\"\xb6\x02\n\x04Type\x12\x0f\n\x0bTYPE_DOUBLE\x10\x01\x12\x0e\n\nTYPE_FLOAT\x10\x02\x12\x0e\n\nTYPE_INT64\x10\x03\x12\x0f\n\x0bTYPE_UINT64\x10\x04\x12\x0e\n\nTYPE_INT32\x10\x05\x12\x10\n\x0cTYPE_FIXED64\x10\x06\x12\x10\n\x0cTYPE_FIXED32\x10\x07\x12\r\n\tTYPE_BOOL\x10\x08\x12\x0f\n\x0bTYPE_STRING\x10\t\x12\x0e\n\nTYPE_GROUP\x10\n\x12\x10\n\x0cTYPE_MESSAGE\x10\x0b\x12\x0e\n\nTYPE_BYTES\x10\x0c\x12\x0f\n\x0bTYPE_UINT32\x10\r\x12\r\n\tTYPE_ENUM\x10\x0e\x12\x11\n\rTYPE_SFIXED32\x10\x0f\x12\x11\n\rTYPE_SFIXED64\x10\x10\x12\x0f\n\x0bTYPE_SINT32\x10\x11\x12\x0f\n\x0bTYPE_SINT64\x10\x12\"C\n\x05Label\x12\x12\n\x0eLABEL_OPTIONAL\x10\x01\x12\x12\n\x0eLABEL_REQUIRED\x10\x02\x12\x12\n\x0eLABEL_REPEATED\x10\x03\"T\n\x14OneofDescriptorProto\x12\x0c\n\x04name\x18\x01 \x01(\t\x12.\n\x07options\x18\x02 \x01(\x0b\x32\x1d.google.protobuf.OneofOptions\"\xa4\x02\n\x13\x45numDescriptorProto\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x38\n\x05value\x18\x02 \x03(\x0b\x32).google.protobuf.EnumValueDescriptorProto\x12-\n\x07options\x18\x03 \x01(\x0b\x32\x1c.google.protobuf.EnumOptions\x12N\n\x0ereserved_range\x18\x04 \x03(\x0b\x32\x36.google.protobuf.EnumDescriptorProto.EnumReservedRange\x12\x15\n\rreserved_name\x18\x05 \x03(\t\x1a/\n\x11\x45numReservedRange\x12\r\n\x05start\x18\x01 \x01(\x05\x12\x0b\n\x03\x65nd\x18\x02 \x01(\x05\"l\n\x18\x45numValueDescriptorProto\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0e\n\x06number\x18\x02 \x01(\x05\x12\x32\n\x07options\x18\x03 \x01(\x0b\x32!.google.protobuf.EnumValueOptions\"\x90\x01\n\x16ServiceDescriptorProto\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x36\n\x06method\x18\x02 \x03(\x0b\x32&.google.protobuf.MethodDescriptorProto\x12\x30\n\x07options\x18\x03 \x01(\x0b\x32\x1f.google.protobuf.ServiceOptions\"\xc1\x01\n\x15MethodDescriptorProto\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x12\n\ninput_type\x18\x02 \x01(\t\x12\x13\n\x0boutput_type\x18\x03 \x01(\t\x12/\n\x07options\x18\x04 \x01(\x0b\x32\x1e.google.protobuf.MethodOptions\x12\x1f\n\x10\x63lient_streaming\x18\x05 \x01(\x08:\x05\x66\x61lse\x12\x1f\n\x10server_streaming\x18\x06 \x01(\x08:\x05\x66\x61lse\"\xa5\x06\n\x0b\x46ileOptions\x12\x14\n\x0cjava_package\x18\x01 \x01(\t\x12\x1c\n\x14java_outer_classname\x18\x08 \x01(\t\x12\"\n\x13java_multiple_files\x18\n \x01(\x08:\x05\x66\x61lse\x12)\n\x1djava_generate_equals_and_hash\x18\x14 \x01(\x08\x42\x02\x18\x01\x12%\n\x16java_string_check_utf8\x18\x1b \x01(\x08:\x05\x66\x61lse\x12\x46\n\x0coptimize_for\x18\t \x01(\x0e\x32).google.protobuf.FileOptions.OptimizeMode:\x05SPEED\x12\x12\n\ngo_package\x18\x0b \x01(\t\x12\"\n\x13\x63\x63_generic_services\x18\x10 \x01(\x08:\x05\x66\x61lse\x12$\n\x15java_generic_services\x18\x11 \x01(\x08:\x05\x66\x61lse\x12\"\n\x13py_generic_services\x18\x12 \x01(\x08:\x05\x66\x61lse\x12#\n\x14php_generic_services\x18* \x01(\x08:\x05\x66\x61lse\x12\x19\n\ndeprecated\x18\x17 \x01(\x08:\x05\x66\x61lse\x12\x1e\n\x10\x63\x63_enable_arenas\x18\x1f \x01(\x08:\x04true\x12\x19\n\x11objc_class_prefix\x18$ \x01(\t\x12\x18\n\x10\x63sharp_namespace\x18% \x01(\t\x12\x14\n\x0cswift_prefix\x18\' \x01(\t\x12\x18\n\x10php_class_prefix\x18( \x01(\t\x12\x15\n\rphp_namespace\x18) \x01(\t\x12\x1e\n\x16php_metadata_namespace\x18, \x01(\t\x12\x14\n\x0cruby_package\x18- \x01(\t\x12\x43\n\x14uninterpreted_option\x18\xe7\x07 \x03(\x0b\x32$.google.protobuf.UninterpretedOption\":\n\x0cOptimizeMode\x12\t\n\x05SPEED\x10\x01\x12\r\n\tCODE_SIZE\x10\x02\x12\x10\n\x0cLITE_RUNTIME\x10\x03*\t\x08\xe8\x07\x10\x80\x80\x80\x80\x02J\x04\x08&\x10\'\"\x84\x02\n\x0eMessageOptions\x12&\n\x17message_set_wire_format\x18\x01 \x01(\x08:\x05\x66\x61lse\x12.\n\x1fno_standard_descriptor_accessor\x18\x02 \x01(\x08:\x05\x66\x61lse\x12\x19\n\ndeprecated\x18\x03 \x01(\x08:\x05\x66\x61lse\x12\x11\n\tmap_entry\x18\x07 \x01(\x08\x12\x43\n\x14uninterpreted_option\x18\xe7\x07 \x03(\x0b\x32$.google.protobuf.UninterpretedOption*\t\x08\xe8\x07\x10\x80\x80\x80\x80\x02J\x04\x08\x04\x10\x05J\x04\x08\x05\x10\x06J\x04\x08\x06\x10\x07J\x04\x08\x08\x10\tJ\x04\x08\t\x10\n\"\xbe\x03\n\x0c\x46ieldOptions\x12:\n\x05\x63type\x18\x01 \x01(\x0e\x32#.google.protobuf.FieldOptions.CType:\x06STRING\x12\x0e\n\x06packed\x18\x02 \x01(\x08\x12?\n\x06jstype\x18\x06 \x01(\x0e\x32$.google.protobuf.FieldOptions.JSType:\tJS_NORMAL\x12\x13\n\x04lazy\x18\x05 \x01(\x08:\x05\x66\x61lse\x12\x1e\n\x0funverified_lazy\x18\x0f \x01(\x08:\x05\x66\x61lse\x12\x19\n\ndeprecated\x18\x03 \x01(\x08:\x05\x66\x61lse\x12\x13\n\x04weak\x18\n \x01(\x08:\x05\x66\x61lse\x12\x43\n\x14uninterpreted_option\x18\xe7\x07 \x03(\x0b\x32$.google.protobuf.UninterpretedOption\"/\n\x05\x43Type\x12\n\n\x06STRING\x10\x00\x12\x08\n\x04\x43ORD\x10\x01\x12\x10\n\x0cSTRING_PIECE\x10\x02\"5\n\x06JSType\x12\r\n\tJS_NORMAL\x10\x00\x12\r\n\tJS_STRING\x10\x01\x12\r\n\tJS_NUMBER\x10\x02*\t\x08\xe8\x07\x10\x80\x80\x80\x80\x02J\x04\x08\x04\x10\x05\"^\n\x0cOneofOptions\x12\x43\n\x14uninterpreted_option\x18\xe7\x07 \x03(\x0b\x32$.google.protobuf.UninterpretedOption*\t\x08\xe8\x07\x10\x80\x80\x80\x80\x02\"\x93\x01\n\x0b\x45numOptions\x12\x13\n\x0b\x61llow_alias\x18\x02 \x01(\x08\x12\x19\n\ndeprecated\x18\x03 \x01(\x08:\x05\x66\x61lse\x12\x43\n\x14uninterpreted_option\x18\xe7\x07 \x03(\x0b\x32$.google.protobuf.UninterpretedOption*\t\x08\xe8\x07\x10\x80\x80\x80\x80\x02J\x04\x08\x05\x10\x06\"}\n\x10\x45numValueOptions\x12\x19\n\ndeprecated\x18\x01 \x01(\x08:\x05\x66\x61lse\x12\x43\n\x14uninterpreted_option\x18\xe7\x07 \x03(\x0b\x32$.google.protobuf.UninterpretedOption*\t\x08\xe8\x07\x10\x80\x80\x80\x80\x02\"{\n\x0eServiceOptions\x12\x19\n\ndeprecated\x18! \x01(\x08:\x05\x66\x61lse\x12\x43\n\x14uninterpreted_option\x18\xe7\x07 \x03(\x0b\x32$.google.protobuf.UninterpretedOption*\t\x08\xe8\x07\x10\x80\x80\x80\x80\x02\"\xad\x02\n\rMethodOptions\x12\x19\n\ndeprecated\x18! \x01(\x08:\x05\x66\x61lse\x12_\n\x11idempotency_level\x18\" \x01(\x0e\x32/.google.protobuf.MethodOptions.IdempotencyLevel:\x13IDEMPOTENCY_UNKNOWN\x12\x43\n\x14uninterpreted_option\x18\xe7\x07 \x03(\x0b\x32$.google.protobuf.UninterpretedOption\"P\n\x10IdempotencyLevel\x12\x17\n\x13IDEMPOTENCY_UNKNOWN\x10\x00\x12\x13\n\x0fNO_SIDE_EFFECTS\x10\x01\x12\x0e\n\nIDEMPOTENT\x10\x02*\t\x08\xe8\x07\x10\x80\x80\x80\x80\x02\"\x9e\x02\n\x13UninterpretedOption\x12;\n\x04name\x18\x02 \x03(\x0b\x32-.google.protobuf.UninterpretedOption.NamePart\x12\x18\n\x10identifier_value\x18\x03 \x01(\t\x12\x1a\n\x12positive_int_value\x18\x04 \x01(\x04\x12\x1a\n\x12negative_int_value\x18\x05 \x01(\x03\x12\x14\n\x0c\x64ouble_value\x18\x06 \x01(\x01\x12\x14\n\x0cstring_value\x18\x07 \x01(\x0c\x12\x17\n\x0f\x61ggregate_value\x18\x08 \x01(\t\x1a\x33\n\x08NamePart\x12\x11\n\tname_part\x18\x01 \x02(\t\x12\x14\n\x0cis_extension\x18\x02 \x02(\x08\"\xd5\x01\n\x0eSourceCodeInfo\x12:\n\x08location\x18\x01 \x03(\x0b\x32(.google.protobuf.SourceCodeInfo.Location\x1a\x86\x01\n\x08Location\x12\x10\n\x04path\x18\x01 \x03(\x05\x42\x02\x10\x01\x12\x10\n\x04span\x18\x02 \x03(\x05\x42\x02\x10\x01\x12\x18\n\x10leading_comments\x18\x03 \x01(\t\x12\x19\n\x11trailing_comments\x18\x04 \x01(\t\x12!\n\x19leading_detached_comments\x18\x06 \x03(\t\"\xa7\x01\n\x11GeneratedCodeInfo\x12\x41\n\nannotation\x18\x01 \x03(\x0b\x32-.google.protobuf.GeneratedCodeInfo.Annotation\x1aO\n\nAnnotation\x12\x10\n\x04path\x18\x01 \x03(\x05\x42\x02\x10\x01\x12\x13\n\x0bsource_file\x18\x02 \x01(\t\x12\r\n\x05\x62\x65gin\x18\x03 \x01(\x05\x12\x0b\n\x03\x65nd\x18\x04 \x01(\x05\x42~\n\x13\x63om.google.protobufB\x10\x44\x65scriptorProtosH\x01Z-google.golang.org/protobuf/types/descriptorpb\xf8\x01\x01\xa2\x02\x03GPB\xaa\x02\x1aGoogle.Protobuf.Reflection') if _descriptor._USE_C_DESCRIPTORS == False: _FIELDDESCRIPTORPROTO_TYPE = _descriptor.EnumDescriptor( name='Type', full_name='google.protobuf.FieldDescriptorProto.Type', filename=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key, values=[ _descriptor.EnumValueDescriptor( name='TYPE_DOUBLE', index=0, number=1, serialized_options=None, type=None, create_key=_descriptor._internal_create_key), _descriptor.EnumValueDescriptor( name='TYPE_FLOAT', index=1, number=2, serialized_options=None, type=None, create_key=_descriptor._internal_create_key), _descriptor.EnumValueDescriptor( name='TYPE_INT64', index=2, number=3, serialized_options=None, type=None, create_key=_descriptor._internal_create_key), _descriptor.EnumValueDescriptor( name='TYPE_UINT64', index=3, number=4, serialized_options=None, type=None, create_key=_descriptor._internal_create_key), _descriptor.EnumValueDescriptor( name='TYPE_INT32', index=4, number=5, serialized_options=None, type=None, create_key=_descriptor._internal_create_key), _descriptor.EnumValueDescriptor( name='TYPE_FIXED64', index=5, number=6, serialized_options=None, type=None, create_key=_descriptor._internal_create_key), _descriptor.EnumValueDescriptor( name='TYPE_FIXED32', index=6, number=7, serialized_options=None, type=None, create_key=_descriptor._internal_create_key), _descriptor.EnumValueDescriptor( name='TYPE_BOOL', index=7, number=8, serialized_options=None, type=None, create_key=_descriptor._internal_create_key), _descriptor.EnumValueDescriptor( name='TYPE_STRING', index=8, number=9, serialized_options=None, type=None, create_key=_descriptor._internal_create_key), _descriptor.EnumValueDescriptor( name='TYPE_GROUP', index=9, number=10, serialized_options=None, type=None, create_key=_descriptor._internal_create_key), _descriptor.EnumValueDescriptor( name='TYPE_MESSAGE', index=10, number=11, serialized_options=None, type=None, create_key=_descriptor._internal_create_key), _descriptor.EnumValueDescriptor( name='TYPE_BYTES', index=11, number=12, serialized_options=None, type=None, create_key=_descriptor._internal_create_key), _descriptor.EnumValueDescriptor( name='TYPE_UINT32', index=12, number=13, serialized_options=None, type=None, create_key=_descriptor._internal_create_key), _descriptor.EnumValueDescriptor( name='TYPE_ENUM', index=13, number=14, serialized_options=None, type=None, create_key=_descriptor._internal_create_key), _descriptor.EnumValueDescriptor( name='TYPE_SFIXED32', index=14, number=15, serialized_options=None, type=None, create_key=_descriptor._internal_create_key), _descriptor.EnumValueDescriptor( name='TYPE_SFIXED64', index=15, number=16, serialized_options=None, type=None, create_key=_descriptor._internal_create_key), _descriptor.EnumValueDescriptor( name='TYPE_SINT32', index=16, number=17, serialized_options=None, type=None, create_key=_descriptor._internal_create_key), _descriptor.EnumValueDescriptor( name='TYPE_SINT64', index=17, number=18, serialized_options=None, type=None, create_key=_descriptor._internal_create_key), ], containing_type=None, serialized_options=None, ) _sym_db.RegisterEnumDescriptor(_FIELDDESCRIPTORPROTO_TYPE) _FIELDDESCRIPTORPROTO_LABEL = _descriptor.EnumDescriptor( name='Label', full_name='google.protobuf.FieldDescriptorProto.Label', filename=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key, values=[ _descriptor.EnumValueDescriptor( name='LABEL_OPTIONAL', index=0, number=1, serialized_options=None, type=None, create_key=_descriptor._internal_create_key), _descriptor.EnumValueDescriptor( name='LABEL_REQUIRED', index=1, number=2, serialized_options=None, type=None, create_key=_descriptor._internal_create_key), _descriptor.EnumValueDescriptor( name='LABEL_REPEATED', index=2, number=3, serialized_options=None, type=None, create_key=_descriptor._internal_create_key), ], containing_type=None, serialized_options=None, ) _sym_db.RegisterEnumDescriptor(_FIELDDESCRIPTORPROTO_LABEL) _FILEOPTIONS_OPTIMIZEMODE = _descriptor.EnumDescriptor( name='OptimizeMode', full_name='google.protobuf.FileOptions.OptimizeMode', filename=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key, values=[ _descriptor.EnumValueDescriptor( name='SPEED', index=0, number=1, serialized_options=None, type=None, create_key=_descriptor._internal_create_key), _descriptor.EnumValueDescriptor( name='CODE_SIZE', index=1, number=2, serialized_options=None, type=None, create_key=_descriptor._internal_create_key), _descriptor.EnumValueDescriptor( name='LITE_RUNTIME', index=2, number=3, serialized_options=None, type=None, create_key=_descriptor._internal_create_key), ], containing_type=None, serialized_options=None, ) _sym_db.RegisterEnumDescriptor(_FILEOPTIONS_OPTIMIZEMODE) _FIELDOPTIONS_CTYPE = _descriptor.EnumDescriptor( name='CType', full_name='google.protobuf.FieldOptions.CType', filename=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key, values=[ _descriptor.EnumValueDescriptor( name='STRING', index=0, number=0, serialized_options=None, type=None, create_key=_descriptor._internal_create_key), _descriptor.EnumValueDescriptor( name='CORD', index=1, number=1, serialized_options=None, type=None, create_key=_descriptor._internal_create_key), _descriptor.EnumValueDescriptor( name='STRING_PIECE', index=2, number=2, serialized_options=None, type=None, create_key=_descriptor._internal_create_key), ], containing_type=None, serialized_options=None, ) _sym_db.RegisterEnumDescriptor(_FIELDOPTIONS_CTYPE) _FIELDOPTIONS_JSTYPE = _descriptor.EnumDescriptor( name='JSType', full_name='google.protobuf.FieldOptions.JSType', filename=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key, values=[ _descriptor.EnumValueDescriptor( name='JS_NORMAL', index=0, number=0, serialized_options=None, type=None, create_key=_descriptor._internal_create_key), _descriptor.EnumValueDescriptor( name='JS_STRING', index=1, number=1, serialized_options=None, type=None, create_key=_descriptor._internal_create_key), _descriptor.EnumValueDescriptor( name='JS_NUMBER', index=2, number=2, serialized_options=None, type=None, create_key=_descriptor._internal_create_key), ], containing_type=None, serialized_options=None, ) _sym_db.RegisterEnumDescriptor(_FIELDOPTIONS_JSTYPE) _METHODOPTIONS_IDEMPOTENCYLEVEL = _descriptor.EnumDescriptor( name='IdempotencyLevel', full_name='google.protobuf.MethodOptions.IdempotencyLevel', filename=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key, values=[ _descriptor.EnumValueDescriptor( name='IDEMPOTENCY_UNKNOWN', index=0, number=0, serialized_options=None, type=None, create_key=_descriptor._internal_create_key), _descriptor.EnumValueDescriptor( name='NO_SIDE_EFFECTS', index=1, number=1, serialized_options=None, type=None, create_key=_descriptor._internal_create_key), _descriptor.EnumValueDescriptor( name='IDEMPOTENT', index=2, number=2, serialized_options=None, type=None, create_key=_descriptor._internal_create_key), ], containing_type=None, serialized_options=None, ) _sym_db.RegisterEnumDescriptor(_METHODOPTIONS_IDEMPOTENCYLEVEL) _FILEDESCRIPTORSET = _descriptor.Descriptor( name='FileDescriptorSet', full_name='google.protobuf.FileDescriptorSet', filename=None, file=DESCRIPTOR, containing_type=None, create_key=_descriptor._internal_create_key, fields=[ _descriptor.FieldDescriptor( name='file', full_name='google.protobuf.FileDescriptorSet.file', index=0, number=1, type=11, cpp_type=10, label=3, has_default_value=False, default_value=[], message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), ], extensions=[ ], nested_types=[], enum_types=[ ], serialized_options=None, is_extendable=False, syntax='proto2', extension_ranges=[], oneofs=[ ], ) _FILEDESCRIPTORPROTO = _descriptor.Descriptor( name='FileDescriptorProto', full_name='google.protobuf.FileDescriptorProto', filename=None, file=DESCRIPTOR, containing_type=None, create_key=_descriptor._internal_create_key, fields=[ _descriptor.FieldDescriptor( name='name', full_name='google.protobuf.FileDescriptorProto.name', index=0, number=1, type=9, cpp_type=9, label=1, has_default_value=False, default_value=b"".decode('utf-8'), message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( name='package', full_name='google.protobuf.FileDescriptorProto.package', index=1, number=2, type=9, cpp_type=9, label=1, has_default_value=False, default_value=b"".decode('utf-8'), message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( name='dependency', full_name='google.protobuf.FileDescriptorProto.dependency', index=2, number=3, type=9, cpp_type=9, label=3, has_default_value=False, default_value=[], message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( name='public_dependency', full_name='google.protobuf.FileDescriptorProto.public_dependency', index=3, number=10, type=5, cpp_type=1, label=3, has_default_value=False, default_value=[], message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( name='weak_dependency', full_name='google.protobuf.FileDescriptorProto.weak_dependency', index=4, number=11, type=5, cpp_type=1, label=3, has_default_value=False, default_value=[], message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( name='message_type', full_name='google.protobuf.FileDescriptorProto.message_type', index=5, number=4, type=11, cpp_type=10, label=3, has_default_value=False, default_value=[], message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( name='enum_type', full_name='google.protobuf.FileDescriptorProto.enum_type', index=6, number=5, type=11, cpp_type=10, label=3, has_default_value=False, default_value=[], message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( name='service', full_name='google.protobuf.FileDescriptorProto.service', index=7, number=6, type=11, cpp_type=10, label=3, has_default_value=False, default_value=[], message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( name='extension', full_name='google.protobuf.FileDescriptorProto.extension', index=8, number=7, type=11, cpp_type=10, label=3, has_default_value=False, default_value=[], message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( name='options', full_name='google.protobuf.FileDescriptorProto.options', index=9, number=8, type=11, cpp_type=10, label=1, has_default_value=False, default_value=None, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( name='source_code_info', full_name='google.protobuf.FileDescriptorProto.source_code_info', index=10, number=9, type=11, cpp_type=10, label=1, has_default_value=False, default_value=None, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( name='syntax', full_name='google.protobuf.FileDescriptorProto.syntax', index=11, number=12, type=9, cpp_type=9, label=1, has_default_value=False, default_value=b"".decode('utf-8'), message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), ], extensions=[ ], nested_types=[], enum_types=[ ], serialized_options=None, is_extendable=False, syntax='proto2', extension_ranges=[], oneofs=[ ], ) _DESCRIPTORPROTO_EXTENSIONRANGE = _descriptor.Descriptor( name='ExtensionRange', full_name='google.protobuf.DescriptorProto.ExtensionRange', filename=None, file=DESCRIPTOR, containing_type=None, create_key=_descriptor._internal_create_key, fields=[ _descriptor.FieldDescriptor( name='start', full_name='google.protobuf.DescriptorProto.ExtensionRange.start', index=0, number=1, type=5, cpp_type=1, label=1, has_default_value=False, default_value=0, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( name='end', full_name='google.protobuf.DescriptorProto.ExtensionRange.end', index=1, number=2, type=5, cpp_type=1, label=1, has_default_value=False, default_value=0, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( name='options', full_name='google.protobuf.DescriptorProto.ExtensionRange.options', index=2, number=3, type=11, cpp_type=10, label=1, has_default_value=False, default_value=None, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), ], extensions=[ ], nested_types=[], enum_types=[ ], serialized_options=None, is_extendable=False, syntax='proto2', extension_ranges=[], oneofs=[ ], ) _DESCRIPTORPROTO_RESERVEDRANGE = _descriptor.Descriptor( name='ReservedRange', full_name='google.protobuf.DescriptorProto.ReservedRange', filename=None, file=DESCRIPTOR, containing_type=None, create_key=_descriptor._internal_create_key, fields=[ _descriptor.FieldDescriptor( name='start', full_name='google.protobuf.DescriptorProto.ReservedRange.start', index=0, number=1, type=5, cpp_type=1, label=1, has_default_value=False, default_value=0, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( name='end', full_name='google.protobuf.DescriptorProto.ReservedRange.end', index=1, number=2, type=5, cpp_type=1, label=1, has_default_value=False, default_value=0, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), ], extensions=[ ], nested_types=[], enum_types=[ ], serialized_options=None, is_extendable=False, syntax='proto2', extension_ranges=[], oneofs=[ ], ) _DESCRIPTORPROTO = _descriptor.Descriptor( name='DescriptorProto', full_name='google.protobuf.DescriptorProto', filename=None, file=DESCRIPTOR, containing_type=None, create_key=_descriptor._internal_create_key, fields=[ _descriptor.FieldDescriptor( name='name', full_name='google.protobuf.DescriptorProto.name', index=0, number=1, type=9, cpp_type=9, label=1, has_default_value=False, default_value=b"".decode('utf-8'), message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( name='field', full_name='google.protobuf.DescriptorProto.field', index=1, number=2, type=11, cpp_type=10, label=3, has_default_value=False, default_value=[], message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( name='extension', full_name='google.protobuf.DescriptorProto.extension', index=2, number=6, type=11, cpp_type=10, label=3, has_default_value=False, default_value=[], message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( name='nested_type', full_name='google.protobuf.DescriptorProto.nested_type', index=3, number=3, type=11, cpp_type=10, label=3, has_default_value=False, default_value=[], message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( name='enum_type', full_name='google.protobuf.DescriptorProto.enum_type', index=4, number=4, type=11, cpp_type=10, label=3, has_default_value=False, default_value=[], message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( name='extension_range', full_name='google.protobuf.DescriptorProto.extension_range', index=5, number=5, type=11, cpp_type=10, label=3, has_default_value=False, default_value=[], message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( name='oneof_decl', full_name='google.protobuf.DescriptorProto.oneof_decl', index=6, number=8, type=11, cpp_type=10, label=3, has_default_value=False, default_value=[], message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( name='options', full_name='google.protobuf.DescriptorProto.options', index=7, number=7, type=11, cpp_type=10, label=1, has_default_value=False, default_value=None, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( name='reserved_range', full_name='google.protobuf.DescriptorProto.reserved_range', index=8, number=9, type=11, cpp_type=10, label=3, has_default_value=False, default_value=[], message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( name='reserved_name', full_name='google.protobuf.DescriptorProto.reserved_name', index=9, number=10, type=9, cpp_type=9, label=3, has_default_value=False, default_value=[], message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), ], extensions=[ ], nested_types=[_DESCRIPTORPROTO_EXTENSIONRANGE, _DESCRIPTORPROTO_RESERVEDRANGE, ], enum_types=[ ], serialized_options=None, is_extendable=False, syntax='proto2', extension_ranges=[], oneofs=[ ], ) _EXTENSIONRANGEOPTIONS = _descriptor.Descriptor( name='ExtensionRangeOptions', full_name='google.protobuf.ExtensionRangeOptions', filename=None, file=DESCRIPTOR, containing_type=None, create_key=_descriptor._internal_create_key, fields=[ _descriptor.FieldDescriptor( name='uninterpreted_option', full_name='google.protobuf.ExtensionRangeOptions.uninterpreted_option', index=0, number=999, type=11, cpp_type=10, label=3, has_default_value=False, default_value=[], message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), ], extensions=[ ], nested_types=[], enum_types=[ ], serialized_options=None, is_extendable=True, syntax='proto2', extension_ranges=[(1000, 536870912), ], oneofs=[ ], ) _FIELDDESCRIPTORPROTO = _descriptor.Descriptor( name='FieldDescriptorProto', full_name='google.protobuf.FieldDescriptorProto', filename=None, file=DESCRIPTOR, containing_type=None, create_key=_descriptor._internal_create_key, fields=[ _descriptor.FieldDescriptor( name='name', full_name='google.protobuf.FieldDescriptorProto.name', index=0, number=1, type=9, cpp_type=9, label=1, has_default_value=False, default_value=b"".decode('utf-8'), message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( name='number', full_name='google.protobuf.FieldDescriptorProto.number', index=1, number=3, type=5, cpp_type=1, label=1, has_default_value=False, default_value=0, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( name='label', full_name='google.protobuf.FieldDescriptorProto.label', index=2, number=4, type=14, cpp_type=8, label=1, has_default_value=False, default_value=1, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( name='type', full_name='google.protobuf.FieldDescriptorProto.type', index=3, number=5, type=14, cpp_type=8, label=1, has_default_value=False, default_value=1, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( name='type_name', full_name='google.protobuf.FieldDescriptorProto.type_name', index=4, number=6, type=9, cpp_type=9, label=1, has_default_value=False, default_value=b"".decode('utf-8'), message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( name='extendee', full_name='google.protobuf.FieldDescriptorProto.extendee', index=5, number=2, type=9, cpp_type=9, label=1, has_default_value=False, default_value=b"".decode('utf-8'), message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( name='default_value', full_name='google.protobuf.FieldDescriptorProto.default_value', index=6, number=7, type=9, cpp_type=9, label=1, has_default_value=False, default_value=b"".decode('utf-8'), message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( name='oneof_index', full_name='google.protobuf.FieldDescriptorProto.oneof_index', index=7, number=9, type=5, cpp_type=1, label=1, has_default_value=False, default_value=0, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( name='json_name', full_name='google.protobuf.FieldDescriptorProto.json_name', index=8, number=10, type=9, cpp_type=9, label=1, has_default_value=False, default_value=b"".decode('utf-8'), message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( name='options', full_name='google.protobuf.FieldDescriptorProto.options', index=9, number=8, type=11, cpp_type=10, label=1, has_default_value=False, default_value=None, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( name='proto3_optional', full_name='google.protobuf.FieldDescriptorProto.proto3_optional', index=10, number=17, type=8, cpp_type=7, label=1, has_default_value=False, default_value=False, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), ], extensions=[ ], nested_types=[], enum_types=[ _FIELDDESCRIPTORPROTO_TYPE, _FIELDDESCRIPTORPROTO_LABEL, ], serialized_options=None, is_extendable=False, syntax='proto2', extension_ranges=[], oneofs=[ ], ) _ONEOFDESCRIPTORPROTO = _descriptor.Descriptor( name='OneofDescriptorProto', full_name='google.protobuf.OneofDescriptorProto', filename=None, file=DESCRIPTOR, containing_type=None, create_key=_descriptor._internal_create_key, fields=[ _descriptor.FieldDescriptor( name='name', full_name='google.protobuf.OneofDescriptorProto.name', index=0, number=1, type=9, cpp_type=9, label=1, has_default_value=False, default_value=b"".decode('utf-8'), message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( name='options', full_name='google.protobuf.OneofDescriptorProto.options', index=1, number=2, type=11, cpp_type=10, label=1, has_default_value=False, default_value=None, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), ], extensions=[ ], nested_types=[], enum_types=[ ], serialized_options=None, is_extendable=False, syntax='proto2', extension_ranges=[], oneofs=[ ], ) _ENUMDESCRIPTORPROTO_ENUMRESERVEDRANGE = _descriptor.Descriptor( name='EnumReservedRange', full_name='google.protobuf.EnumDescriptorProto.EnumReservedRange', filename=None, file=DESCRIPTOR, containing_type=None, create_key=_descriptor._internal_create_key, fields=[ _descriptor.FieldDescriptor( name='start', full_name='google.protobuf.EnumDescriptorProto.EnumReservedRange.start', index=0, number=1, type=5, cpp_type=1, label=1, has_default_value=False, default_value=0, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( name='end', full_name='google.protobuf.EnumDescriptorProto.EnumReservedRange.end', index=1, number=2, type=5, cpp_type=1, label=1, has_default_value=False, default_value=0, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), ], extensions=[ ], nested_types=[], enum_types=[ ], serialized_options=None, is_extendable=False, syntax='proto2', extension_ranges=[], oneofs=[ ], ) _ENUMDESCRIPTORPROTO = _descriptor.Descriptor( name='EnumDescriptorProto', full_name='google.protobuf.EnumDescriptorProto', filename=None, file=DESCRIPTOR, containing_type=None, create_key=_descriptor._internal_create_key, fields=[ _descriptor.FieldDescriptor( name='name', full_name='google.protobuf.EnumDescriptorProto.name', index=0, number=1, type=9, cpp_type=9, label=1, has_default_value=False, default_value=b"".decode('utf-8'), message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( name='value', full_name='google.protobuf.EnumDescriptorProto.value', index=1, number=2, type=11, cpp_type=10, label=3, has_default_value=False, default_value=[], message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( name='options', full_name='google.protobuf.EnumDescriptorProto.options', index=2, number=3, type=11, cpp_type=10, label=1, has_default_value=False, default_value=None, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( name='reserved_range', full_name='google.protobuf.EnumDescriptorProto.reserved_range', index=3, number=4, type=11, cpp_type=10, label=3, has_default_value=False, default_value=[], message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( name='reserved_name', full_name='google.protobuf.EnumDescriptorProto.reserved_name', index=4, number=5, type=9, cpp_type=9, label=3, has_default_value=False, default_value=[], message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), ], extensions=[ ], nested_types=[_ENUMDESCRIPTORPROTO_ENUMRESERVEDRANGE, ], enum_types=[ ], serialized_options=None, is_extendable=False, syntax='proto2', extension_ranges=[], oneofs=[ ], ) _ENUMVALUEDESCRIPTORPROTO = _descriptor.Descriptor( name='EnumValueDescriptorProto', full_name='google.protobuf.EnumValueDescriptorProto', filename=None, file=DESCRIPTOR, containing_type=None, create_key=_descriptor._internal_create_key, fields=[ _descriptor.FieldDescriptor( name='name', full_name='google.protobuf.EnumValueDescriptorProto.name', index=0, number=1, type=9, cpp_type=9, label=1, has_default_value=False, default_value=b"".decode('utf-8'), message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( name='number', full_name='google.protobuf.EnumValueDescriptorProto.number', index=1, number=2, type=5, cpp_type=1, label=1, has_default_value=False, default_value=0, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( name='options', full_name='google.protobuf.EnumValueDescriptorProto.options', index=2, number=3, type=11, cpp_type=10, label=1, has_default_value=False, default_value=None, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), ], extensions=[ ], nested_types=[], enum_types=[ ], serialized_options=None, is_extendable=False, syntax='proto2', extension_ranges=[], oneofs=[ ], ) _SERVICEDESCRIPTORPROTO = _descriptor.Descriptor( name='ServiceDescriptorProto', full_name='google.protobuf.ServiceDescriptorProto', filename=None, file=DESCRIPTOR, containing_type=None, create_key=_descriptor._internal_create_key, fields=[ _descriptor.FieldDescriptor( name='name', full_name='google.protobuf.ServiceDescriptorProto.name', index=0, number=1, type=9, cpp_type=9, label=1, has_default_value=False, default_value=b"".decode('utf-8'), message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( name='method', full_name='google.protobuf.ServiceDescriptorProto.method', index=1, number=2, type=11, cpp_type=10, label=3, has_default_value=False, default_value=[], message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( name='options', full_name='google.protobuf.ServiceDescriptorProto.options', index=2, number=3, type=11, cpp_type=10, label=1, has_default_value=False, default_value=None, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), ], extensions=[ ], nested_types=[], enum_types=[ ], serialized_options=None, is_extendable=False, syntax='proto2', extension_ranges=[], oneofs=[ ], ) _METHODDESCRIPTORPROTO = _descriptor.Descriptor( name='MethodDescriptorProto', full_name='google.protobuf.MethodDescriptorProto', filename=None, file=DESCRIPTOR, containing_type=None, create_key=_descriptor._internal_create_key, fields=[ _descriptor.FieldDescriptor( name='name', full_name='google.protobuf.MethodDescriptorProto.name', index=0, number=1, type=9, cpp_type=9, label=1, has_default_value=False, default_value=b"".decode('utf-8'), message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( name='input_type', full_name='google.protobuf.MethodDescriptorProto.input_type', index=1, number=2, type=9, cpp_type=9, label=1, has_default_value=False, default_value=b"".decode('utf-8'), message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( name='output_type', full_name='google.protobuf.MethodDescriptorProto.output_type', index=2, number=3, type=9, cpp_type=9, label=1, has_default_value=False, default_value=b"".decode('utf-8'), message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( name='options', full_name='google.protobuf.MethodDescriptorProto.options', index=3, number=4, type=11, cpp_type=10, label=1, has_default_value=False, default_value=None, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( name='client_streaming', full_name='google.protobuf.MethodDescriptorProto.client_streaming', index=4, number=5, type=8, cpp_type=7, label=1, has_default_value=True, default_value=False, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( name='server_streaming', full_name='google.protobuf.MethodDescriptorProto.server_streaming', index=5, number=6, type=8, cpp_type=7, label=1, has_default_value=True, default_value=False, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), ], extensions=[ ], nested_types=[], enum_types=[ ], serialized_options=None, is_extendable=False, syntax='proto2', extension_ranges=[], oneofs=[ ], ) _FILEOPTIONS = _descriptor.Descriptor( name='FileOptions', full_name='google.protobuf.FileOptions', filename=None, file=DESCRIPTOR, containing_type=None, create_key=_descriptor._internal_create_key, fields=[ _descriptor.FieldDescriptor( name='java_package', full_name='google.protobuf.FileOptions.java_package', index=0, number=1, type=9, cpp_type=9, label=1, has_default_value=False, default_value=b"".decode('utf-8'), message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( name='java_outer_classname', full_name='google.protobuf.FileOptions.java_outer_classname', index=1, number=8, type=9, cpp_type=9, label=1, has_default_value=False, default_value=b"".decode('utf-8'), message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( name='java_multiple_files', full_name='google.protobuf.FileOptions.java_multiple_files', index=2, number=10, type=8, cpp_type=7, label=1, has_default_value=True, default_value=False, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( name='java_generate_equals_and_hash', full_name='google.protobuf.FileOptions.java_generate_equals_and_hash', index=3, number=20, type=8, cpp_type=7, label=1, has_default_value=False, default_value=False, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( name='java_string_check_utf8', full_name='google.protobuf.FileOptions.java_string_check_utf8', index=4, number=27, type=8, cpp_type=7, label=1, has_default_value=True, default_value=False, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( name='optimize_for', full_name='google.protobuf.FileOptions.optimize_for', index=5, number=9, type=14, cpp_type=8, label=1, has_default_value=True, default_value=1, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( name='go_package', full_name='google.protobuf.FileOptions.go_package', index=6, number=11, type=9, cpp_type=9, label=1, has_default_value=False, default_value=b"".decode('utf-8'), message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( name='cc_generic_services', full_name='google.protobuf.FileOptions.cc_generic_services', index=7, number=16, type=8, cpp_type=7, label=1, has_default_value=True, default_value=False, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( name='java_generic_services', full_name='google.protobuf.FileOptions.java_generic_services', index=8, number=17, type=8, cpp_type=7, label=1, has_default_value=True, default_value=False, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( name='py_generic_services', full_name='google.protobuf.FileOptions.py_generic_services', index=9, number=18, type=8, cpp_type=7, label=1, has_default_value=True, default_value=False, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( name='php_generic_services', full_name='google.protobuf.FileOptions.php_generic_services', index=10, number=42, type=8, cpp_type=7, label=1, has_default_value=True, default_value=False, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( name='deprecated', full_name='google.protobuf.FileOptions.deprecated', index=11, number=23, type=8, cpp_type=7, label=1, has_default_value=True, default_value=False, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( name='cc_enable_arenas', full_name='google.protobuf.FileOptions.cc_enable_arenas', index=12, number=31, type=8, cpp_type=7, label=1, has_default_value=True, default_value=True, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( name='objc_class_prefix', full_name='google.protobuf.FileOptions.objc_class_prefix', index=13, number=36, type=9, cpp_type=9, label=1, has_default_value=False, default_value=b"".decode('utf-8'), message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( name='csharp_namespace', full_name='google.protobuf.FileOptions.csharp_namespace', index=14, number=37, type=9, cpp_type=9, label=1, has_default_value=False, default_value=b"".decode('utf-8'), message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( name='swift_prefix', full_name='google.protobuf.FileOptions.swift_prefix', index=15, number=39, type=9, cpp_type=9, label=1, has_default_value=False, default_value=b"".decode('utf-8'), message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( name='php_class_prefix', full_name='google.protobuf.FileOptions.php_class_prefix', index=16, number=40, type=9, cpp_type=9, label=1, has_default_value=False, default_value=b"".decode('utf-8'), message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( name='php_namespace', full_name='google.protobuf.FileOptions.php_namespace', index=17, number=41, type=9, cpp_type=9, label=1, has_default_value=False, default_value=b"".decode('utf-8'), message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( name='php_metadata_namespace', full_name='google.protobuf.FileOptions.php_metadata_namespace', index=18, number=44, type=9, cpp_type=9, label=1, has_default_value=False, default_value=b"".decode('utf-8'), message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( name='ruby_package', full_name='google.protobuf.FileOptions.ruby_package', index=19, number=45, type=9, cpp_type=9, label=1, has_default_value=False, default_value=b"".decode('utf-8'), message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( name='uninterpreted_option', full_name='google.protobuf.FileOptions.uninterpreted_option', index=20, number=999, type=11, cpp_type=10, label=3, has_default_value=False, default_value=[], message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), ], extensions=[ ], nested_types=[], enum_types=[ _FILEOPTIONS_OPTIMIZEMODE, ], serialized_options=None, is_extendable=True, syntax='proto2', extension_ranges=[(1000, 536870912), ], oneofs=[ ], ) _MESSAGEOPTIONS = _descriptor.Descriptor( name='MessageOptions', full_name='google.protobuf.MessageOptions', filename=None, file=DESCRIPTOR, containing_type=None, create_key=_descriptor._internal_create_key, fields=[ _descriptor.FieldDescriptor( name='message_set_wire_format', full_name='google.protobuf.MessageOptions.message_set_wire_format', index=0, number=1, type=8, cpp_type=7, label=1, has_default_value=True, default_value=False, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( name='no_standard_descriptor_accessor', full_name='google.protobuf.MessageOptions.no_standard_descriptor_accessor', index=1, number=2, type=8, cpp_type=7, label=1, has_default_value=True, default_value=False, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( name='deprecated', full_name='google.protobuf.MessageOptions.deprecated', index=2, number=3, type=8, cpp_type=7, label=1, has_default_value=True, default_value=False, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( name='map_entry', full_name='google.protobuf.MessageOptions.map_entry', index=3, number=7, type=8, cpp_type=7, label=1, has_default_value=False, default_value=False, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( name='uninterpreted_option', full_name='google.protobuf.MessageOptions.uninterpreted_option', index=4, number=999, type=11, cpp_type=10, label=3, has_default_value=False, default_value=[], message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), ], extensions=[ ], nested_types=[], enum_types=[ ], serialized_options=None, is_extendable=True, syntax='proto2', extension_ranges=[(1000, 536870912), ], oneofs=[ ], ) _FIELDOPTIONS = _descriptor.Descriptor( name='FieldOptions', full_name='google.protobuf.FieldOptions', filename=None, file=DESCRIPTOR, containing_type=None, create_key=_descriptor._internal_create_key, fields=[ _descriptor.FieldDescriptor( name='ctype', full_name='google.protobuf.FieldOptions.ctype', index=0, number=1, type=14, cpp_type=8, label=1, has_default_value=True, default_value=0, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( name='packed', full_name='google.protobuf.FieldOptions.packed', index=1, number=2, type=8, cpp_type=7, label=1, has_default_value=False, default_value=False, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( name='jstype', full_name='google.protobuf.FieldOptions.jstype', index=2, number=6, type=14, cpp_type=8, label=1, has_default_value=True, default_value=0, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( name='lazy', full_name='google.protobuf.FieldOptions.lazy', index=3, number=5, type=8, cpp_type=7, label=1, has_default_value=True, default_value=False, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( name='unverified_lazy', full_name='google.protobuf.FieldOptions.unverified_lazy', index=4, number=15, type=8, cpp_type=7, label=1, has_default_value=True, default_value=False, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( name='deprecated', full_name='google.protobuf.FieldOptions.deprecated', index=5, number=3, type=8, cpp_type=7, label=1, has_default_value=True, default_value=False, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( name='weak', full_name='google.protobuf.FieldOptions.weak', index=6, number=10, type=8, cpp_type=7, label=1, has_default_value=True, default_value=False, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( name='uninterpreted_option', full_name='google.protobuf.FieldOptions.uninterpreted_option', index=7, number=999, type=11, cpp_type=10, label=3, has_default_value=False, default_value=[], message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), ], extensions=[ ], nested_types=[], enum_types=[ _FIELDOPTIONS_CTYPE, _FIELDOPTIONS_JSTYPE, ], serialized_options=None, is_extendable=True, syntax='proto2', extension_ranges=[(1000, 536870912), ], oneofs=[ ], ) _ONEOFOPTIONS = _descriptor.Descriptor( name='OneofOptions', full_name='google.protobuf.OneofOptions', filename=None, file=DESCRIPTOR, containing_type=None, create_key=_descriptor._internal_create_key, fields=[ _descriptor.FieldDescriptor( name='uninterpreted_option', full_name='google.protobuf.OneofOptions.uninterpreted_option', index=0, number=999, type=11, cpp_type=10, label=3, has_default_value=False, default_value=[], message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), ], extensions=[ ], nested_types=[], enum_types=[ ], serialized_options=None, is_extendable=True, syntax='proto2', extension_ranges=[(1000, 536870912), ], oneofs=[ ], ) _ENUMOPTIONS = _descriptor.Descriptor( name='EnumOptions', full_name='google.protobuf.EnumOptions', filename=None, file=DESCRIPTOR, containing_type=None, create_key=_descriptor._internal_create_key, fields=[ _descriptor.FieldDescriptor( name='allow_alias', full_name='google.protobuf.EnumOptions.allow_alias', index=0, number=2, type=8, cpp_type=7, label=1, has_default_value=False, default_value=False, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( name='deprecated', full_name='google.protobuf.EnumOptions.deprecated', index=1, number=3, type=8, cpp_type=7, label=1, has_default_value=True, default_value=False, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( name='uninterpreted_option', full_name='google.protobuf.EnumOptions.uninterpreted_option', index=2, number=999, type=11, cpp_type=10, label=3, has_default_value=False, default_value=[], message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), ], extensions=[ ], nested_types=[], enum_types=[ ], serialized_options=None, is_extendable=True, syntax='proto2', extension_ranges=[(1000, 536870912), ], oneofs=[ ], ) _ENUMVALUEOPTIONS = _descriptor.Descriptor( name='EnumValueOptions', full_name='google.protobuf.EnumValueOptions', filename=None, file=DESCRIPTOR, containing_type=None, create_key=_descriptor._internal_create_key, fields=[ _descriptor.FieldDescriptor( name='deprecated', full_name='google.protobuf.EnumValueOptions.deprecated', index=0, number=1, type=8, cpp_type=7, label=1, has_default_value=True, default_value=False, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( name='uninterpreted_option', full_name='google.protobuf.EnumValueOptions.uninterpreted_option', index=1, number=999, type=11, cpp_type=10, label=3, has_default_value=False, default_value=[], message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), ], extensions=[ ], nested_types=[], enum_types=[ ], serialized_options=None, is_extendable=True, syntax='proto2', extension_ranges=[(1000, 536870912), ], oneofs=[ ], ) _SERVICEOPTIONS = _descriptor.Descriptor( name='ServiceOptions', full_name='google.protobuf.ServiceOptions', filename=None, file=DESCRIPTOR, containing_type=None, create_key=_descriptor._internal_create_key, fields=[ _descriptor.FieldDescriptor( name='deprecated', full_name='google.protobuf.ServiceOptions.deprecated', index=0, number=33, type=8, cpp_type=7, label=1, has_default_value=True, default_value=False, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( name='uninterpreted_option', full_name='google.protobuf.ServiceOptions.uninterpreted_option', index=1, number=999, type=11, cpp_type=10, label=3, has_default_value=False, default_value=[], message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), ], extensions=[ ], nested_types=[], enum_types=[ ], serialized_options=None, is_extendable=True, syntax='proto2', extension_ranges=[(1000, 536870912), ], oneofs=[ ], ) _METHODOPTIONS = _descriptor.Descriptor( name='MethodOptions', full_name='google.protobuf.MethodOptions', filename=None, file=DESCRIPTOR, containing_type=None, create_key=_descriptor._internal_create_key, fields=[ _descriptor.FieldDescriptor( name='deprecated', full_name='google.protobuf.MethodOptions.deprecated', index=0, number=33, type=8, cpp_type=7, label=1, has_default_value=True, default_value=False, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( name='idempotency_level', full_name='google.protobuf.MethodOptions.idempotency_level', index=1, number=34, type=14, cpp_type=8, label=1, has_default_value=True, default_value=0, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( name='uninterpreted_option', full_name='google.protobuf.MethodOptions.uninterpreted_option', index=2, number=999, type=11, cpp_type=10, label=3, has_default_value=False, default_value=[], message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), ], extensions=[ ], nested_types=[], enum_types=[ _METHODOPTIONS_IDEMPOTENCYLEVEL, ], serialized_options=None, is_extendable=True, syntax='proto2', extension_ranges=[(1000, 536870912), ], oneofs=[ ], ) _UNINTERPRETEDOPTION_NAMEPART = _descriptor.Descriptor( name='NamePart', full_name='google.protobuf.UninterpretedOption.NamePart', filename=None, file=DESCRIPTOR, containing_type=None, create_key=_descriptor._internal_create_key, fields=[ _descriptor.FieldDescriptor( name='name_part', full_name='google.protobuf.UninterpretedOption.NamePart.name_part', index=0, number=1, type=9, cpp_type=9, label=2, has_default_value=False, default_value=b"".decode('utf-8'), message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( name='is_extension', full_name='google.protobuf.UninterpretedOption.NamePart.is_extension', index=1, number=2, type=8, cpp_type=7, label=2, has_default_value=False, default_value=False, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), ], extensions=[ ], nested_types=[], enum_types=[ ], serialized_options=None, is_extendable=False, syntax='proto2', extension_ranges=[], oneofs=[ ], ) _UNINTERPRETEDOPTION = _descriptor.Descriptor( name='UninterpretedOption', full_name='google.protobuf.UninterpretedOption', filename=None, file=DESCRIPTOR, containing_type=None, create_key=_descriptor._internal_create_key, fields=[ _descriptor.FieldDescriptor( name='name', full_name='google.protobuf.UninterpretedOption.name', index=0, number=2, type=11, cpp_type=10, label=3, has_default_value=False, default_value=[], message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( name='identifier_value', full_name='google.protobuf.UninterpretedOption.identifier_value', index=1, number=3, type=9, cpp_type=9, label=1, has_default_value=False, default_value=b"".decode('utf-8'), message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( name='positive_int_value', full_name='google.protobuf.UninterpretedOption.positive_int_value', index=2, number=4, type=4, cpp_type=4, label=1, has_default_value=False, default_value=0, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( name='negative_int_value', full_name='google.protobuf.UninterpretedOption.negative_int_value', index=3, number=5, type=3, cpp_type=2, label=1, has_default_value=False, default_value=0, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( name='double_value', full_name='google.protobuf.UninterpretedOption.double_value', index=4, number=6, type=1, cpp_type=5, label=1, has_default_value=False, default_value=float(0), message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( name='string_value', full_name='google.protobuf.UninterpretedOption.string_value', index=5, number=7, type=12, cpp_type=9, label=1, has_default_value=False, default_value=b"", message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( name='aggregate_value', full_name='google.protobuf.UninterpretedOption.aggregate_value', index=6, number=8, type=9, cpp_type=9, label=1, has_default_value=False, default_value=b"".decode('utf-8'), message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), ], extensions=[ ], nested_types=[_UNINTERPRETEDOPTION_NAMEPART, ], enum_types=[ ], serialized_options=None, is_extendable=False, syntax='proto2', extension_ranges=[], oneofs=[ ], ) _SOURCECODEINFO_LOCATION = _descriptor.Descriptor( name='Location', full_name='google.protobuf.SourceCodeInfo.Location', filename=None, file=DESCRIPTOR, containing_type=None, create_key=_descriptor._internal_create_key, fields=[ _descriptor.FieldDescriptor( name='path', full_name='google.protobuf.SourceCodeInfo.Location.path', index=0, number=1, type=5, cpp_type=1, label=3, has_default_value=False, default_value=[], message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( name='span', full_name='google.protobuf.SourceCodeInfo.Location.span', index=1, number=2, type=5, cpp_type=1, label=3, has_default_value=False, default_value=[], message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( name='leading_comments', full_name='google.protobuf.SourceCodeInfo.Location.leading_comments', index=2, number=3, type=9, cpp_type=9, label=1, has_default_value=False, default_value=b"".decode('utf-8'), message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( name='trailing_comments', full_name='google.protobuf.SourceCodeInfo.Location.trailing_comments', index=3, number=4, type=9, cpp_type=9, label=1, has_default_value=False, default_value=b"".decode('utf-8'), message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( name='leading_detached_comments', full_name='google.protobuf.SourceCodeInfo.Location.leading_detached_comments', index=4, number=6, type=9, cpp_type=9, label=3, has_default_value=False, default_value=[], message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), ], extensions=[ ], nested_types=[], enum_types=[ ], serialized_options=None, is_extendable=False, syntax='proto2', extension_ranges=[], oneofs=[ ], ) _SOURCECODEINFO = _descriptor.Descriptor( name='SourceCodeInfo', full_name='google.protobuf.SourceCodeInfo', filename=None, file=DESCRIPTOR, containing_type=None, create_key=_descriptor._internal_create_key, fields=[ _descriptor.FieldDescriptor( name='location', full_name='google.protobuf.SourceCodeInfo.location', index=0, number=1, type=11, cpp_type=10, label=3, has_default_value=False, default_value=[], message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), ], extensions=[ ], nested_types=[_SOURCECODEINFO_LOCATION, ], enum_types=[ ], serialized_options=None, is_extendable=False, syntax='proto2', extension_ranges=[], oneofs=[ ], ) _GENERATEDCODEINFO_ANNOTATION = _descriptor.Descriptor( name='Annotation', full_name='google.protobuf.GeneratedCodeInfo.Annotation', filename=None, file=DESCRIPTOR, containing_type=None, create_key=_descriptor._internal_create_key, fields=[ _descriptor.FieldDescriptor( name='path', full_name='google.protobuf.GeneratedCodeInfo.Annotation.path', index=0, number=1, type=5, cpp_type=1, label=3, has_default_value=False, default_value=[], message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( name='source_file', full_name='google.protobuf.GeneratedCodeInfo.Annotation.source_file', index=1, number=2, type=9, cpp_type=9, label=1, has_default_value=False, default_value=b"".decode('utf-8'), message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( name='begin', full_name='google.protobuf.GeneratedCodeInfo.Annotation.begin', index=2, number=3, type=5, cpp_type=1, label=1, has_default_value=False, default_value=0, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( name='end', full_name='google.protobuf.GeneratedCodeInfo.Annotation.end', index=3, number=4, type=5, cpp_type=1, label=1, has_default_value=False, default_value=0, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), ], extensions=[ ], nested_types=[], enum_types=[ ], serialized_options=None, is_extendable=False, syntax='proto2', extension_ranges=[], oneofs=[ ], ) _GENERATEDCODEINFO = _descriptor.Descriptor( name='GeneratedCodeInfo', full_name='google.protobuf.GeneratedCodeInfo', filename=None, file=DESCRIPTOR, containing_type=None, create_key=_descriptor._internal_create_key, fields=[ _descriptor.FieldDescriptor( name='annotation', full_name='google.protobuf.GeneratedCodeInfo.annotation', index=0, number=1, type=11, cpp_type=10, label=3, has_default_value=False, default_value=[], message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), ], extensions=[ ], nested_types=[_GENERATEDCODEINFO_ANNOTATION, ], enum_types=[ ], serialized_options=None, is_extendable=False, syntax='proto2', extension_ranges=[], oneofs=[ ], ) _FILEDESCRIPTORSET.fields_by_name['file'].message_type = _FILEDESCRIPTORPROTO _FILEDESCRIPTORPROTO.fields_by_name['message_type'].message_type = _DESCRIPTORPROTO _FILEDESCRIPTORPROTO.fields_by_name['enum_type'].message_type = _ENUMDESCRIPTORPROTO _FILEDESCRIPTORPROTO.fields_by_name['service'].message_type = _SERVICEDESCRIPTORPROTO _FILEDESCRIPTORPROTO.fields_by_name['extension'].message_type = _FIELDDESCRIPTORPROTO _FILEDESCRIPTORPROTO.fields_by_name['options'].message_type = _FILEOPTIONS _FILEDESCRIPTORPROTO.fields_by_name['source_code_info'].message_type = _SOURCECODEINFO _DESCRIPTORPROTO_EXTENSIONRANGE.fields_by_name['options'].message_type = _EXTENSIONRANGEOPTIONS _DESCRIPTORPROTO_EXTENSIONRANGE.containing_type = _DESCRIPTORPROTO _DESCRIPTORPROTO_RESERVEDRANGE.containing_type = _DESCRIPTORPROTO _DESCRIPTORPROTO.fields_by_name['field'].message_type = _FIELDDESCRIPTORPROTO _DESCRIPTORPROTO.fields_by_name['extension'].message_type = _FIELDDESCRIPTORPROTO _DESCRIPTORPROTO.fields_by_name['nested_type'].message_type = _DESCRIPTORPROTO _DESCRIPTORPROTO.fields_by_name['enum_type'].message_type = _ENUMDESCRIPTORPROTO _DESCRIPTORPROTO.fields_by_name['extension_range'].message_type = _DESCRIPTORPROTO_EXTENSIONRANGE _DESCRIPTORPROTO.fields_by_name['oneof_decl'].message_type = _ONEOFDESCRIPTORPROTO _DESCRIPTORPROTO.fields_by_name['options'].message_type = _MESSAGEOPTIONS _DESCRIPTORPROTO.fields_by_name['reserved_range'].message_type = _DESCRIPTORPROTO_RESERVEDRANGE _EXTENSIONRANGEOPTIONS.fields_by_name['uninterpreted_option'].message_type = _UNINTERPRETEDOPTION _FIELDDESCRIPTORPROTO.fields_by_name['label'].enum_type = _FIELDDESCRIPTORPROTO_LABEL _FIELDDESCRIPTORPROTO.fields_by_name['type'].enum_type = _FIELDDESCRIPTORPROTO_TYPE _FIELDDESCRIPTORPROTO.fields_by_name['options'].message_type = _FIELDOPTIONS _FIELDDESCRIPTORPROTO_TYPE.containing_type = _FIELDDESCRIPTORPROTO _FIELDDESCRIPTORPROTO_LABEL.containing_type = _FIELDDESCRIPTORPROTO _ONEOFDESCRIPTORPROTO.fields_by_name['options'].message_type = _ONEOFOPTIONS _ENUMDESCRIPTORPROTO_ENUMRESERVEDRANGE.containing_type = _ENUMDESCRIPTORPROTO _ENUMDESCRIPTORPROTO.fields_by_name['value'].message_type = _ENUMVALUEDESCRIPTORPROTO _ENUMDESCRIPTORPROTO.fields_by_name['options'].message_type = _ENUMOPTIONS _ENUMDESCRIPTORPROTO.fields_by_name['reserved_range'].message_type = _ENUMDESCRIPTORPROTO_ENUMRESERVEDRANGE _ENUMVALUEDESCRIPTORPROTO.fields_by_name['options'].message_type = _ENUMVALUEOPTIONS _SERVICEDESCRIPTORPROTO.fields_by_name['method'].message_type = _METHODDESCRIPTORPROTO _SERVICEDESCRIPTORPROTO.fields_by_name['options'].message_type = _SERVICEOPTIONS _METHODDESCRIPTORPROTO.fields_by_name['options'].message_type = _METHODOPTIONS _FILEOPTIONS.fields_by_name['optimize_for'].enum_type = _FILEOPTIONS_OPTIMIZEMODE _FILEOPTIONS.fields_by_name['uninterpreted_option'].message_type = _UNINTERPRETEDOPTION _FILEOPTIONS_OPTIMIZEMODE.containing_type = _FILEOPTIONS _MESSAGEOPTIONS.fields_by_name['uninterpreted_option'].message_type = _UNINTERPRETEDOPTION _FIELDOPTIONS.fields_by_name['ctype'].enum_type = _FIELDOPTIONS_CTYPE _FIELDOPTIONS.fields_by_name['jstype'].enum_type = _FIELDOPTIONS_JSTYPE _FIELDOPTIONS.fields_by_name['uninterpreted_option'].message_type = _UNINTERPRETEDOPTION _FIELDOPTIONS_CTYPE.containing_type = _FIELDOPTIONS _FIELDOPTIONS_JSTYPE.containing_type = _FIELDOPTIONS _ONEOFOPTIONS.fields_by_name['uninterpreted_option'].message_type = _UNINTERPRETEDOPTION _ENUMOPTIONS.fields_by_name['uninterpreted_option'].message_type = _UNINTERPRETEDOPTION _ENUMVALUEOPTIONS.fields_by_name['uninterpreted_option'].message_type = _UNINTERPRETEDOPTION _SERVICEOPTIONS.fields_by_name['uninterpreted_option'].message_type = _UNINTERPRETEDOPTION _METHODOPTIONS.fields_by_name['idempotency_level'].enum_type = _METHODOPTIONS_IDEMPOTENCYLEVEL _METHODOPTIONS.fields_by_name['uninterpreted_option'].message_type = _UNINTERPRETEDOPTION _METHODOPTIONS_IDEMPOTENCYLEVEL.containing_type = _METHODOPTIONS _UNINTERPRETEDOPTION_NAMEPART.containing_type = _UNINTERPRETEDOPTION _UNINTERPRETEDOPTION.fields_by_name['name'].message_type = _UNINTERPRETEDOPTION_NAMEPART _SOURCECODEINFO_LOCATION.containing_type = _SOURCECODEINFO _SOURCECODEINFO.fields_by_name['location'].message_type = _SOURCECODEINFO_LOCATION _GENERATEDCODEINFO_ANNOTATION.containing_type = _GENERATEDCODEINFO _GENERATEDCODEINFO.fields_by_name['annotation'].message_type = _GENERATEDCODEINFO_ANNOTATION DESCRIPTOR.message_types_by_name['FileDescriptorSet'] = _FILEDESCRIPTORSET DESCRIPTOR.message_types_by_name['FileDescriptorProto'] = _FILEDESCRIPTORPROTO DESCRIPTOR.message_types_by_name['DescriptorProto'] = _DESCRIPTORPROTO DESCRIPTOR.message_types_by_name['ExtensionRangeOptions'] = _EXTENSIONRANGEOPTIONS DESCRIPTOR.message_types_by_name['FieldDescriptorProto'] = _FIELDDESCRIPTORPROTO DESCRIPTOR.message_types_by_name['OneofDescriptorProto'] = _ONEOFDESCRIPTORPROTO DESCRIPTOR.message_types_by_name['EnumDescriptorProto'] = _ENUMDESCRIPTORPROTO DESCRIPTOR.message_types_by_name['EnumValueDescriptorProto'] = _ENUMVALUEDESCRIPTORPROTO DESCRIPTOR.message_types_by_name['ServiceDescriptorProto'] = _SERVICEDESCRIPTORPROTO DESCRIPTOR.message_types_by_name['MethodDescriptorProto'] = _METHODDESCRIPTORPROTO DESCRIPTOR.message_types_by_name['FileOptions'] = _FILEOPTIONS DESCRIPTOR.message_types_by_name['MessageOptions'] = _MESSAGEOPTIONS DESCRIPTOR.message_types_by_name['FieldOptions'] = _FIELDOPTIONS DESCRIPTOR.message_types_by_name['OneofOptions'] = _ONEOFOPTIONS DESCRIPTOR.message_types_by_name['EnumOptions'] = _ENUMOPTIONS DESCRIPTOR.message_types_by_name['EnumValueOptions'] = _ENUMVALUEOPTIONS DESCRIPTOR.message_types_by_name['ServiceOptions'] = _SERVICEOPTIONS DESCRIPTOR.message_types_by_name['MethodOptions'] = _METHODOPTIONS DESCRIPTOR.message_types_by_name['UninterpretedOption'] = _UNINTERPRETEDOPTION DESCRIPTOR.message_types_by_name['SourceCodeInfo'] = _SOURCECODEINFO DESCRIPTOR.message_types_by_name['GeneratedCodeInfo'] = _GENERATEDCODEINFO _sym_db.RegisterFileDescriptor(DESCRIPTOR) else: _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals()) _builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'google.protobuf.descriptor_pb2', globals()) if _descriptor._USE_C_DESCRIPTORS == False: DESCRIPTOR._options = None _FILEDESCRIPTORSET._serialized_start=53 _FILEDESCRIPTORSET._serialized_end=124 _FILEDESCRIPTORPROTO._serialized_start=127 _FILEDESCRIPTORPROTO._serialized_end=602 _DESCRIPTORPROTO._serialized_start=605 _DESCRIPTORPROTO._serialized_end=1286 _DESCRIPTORPROTO_EXTENSIONRANGE._serialized_start=1140 _DESCRIPTORPROTO_EXTENSIONRANGE._serialized_end=1241 _DESCRIPTORPROTO_RESERVEDRANGE._serialized_start=1243 _DESCRIPTORPROTO_RESERVEDRANGE._serialized_end=1286 _EXTENSIONRANGEOPTIONS._serialized_start=1288 _EXTENSIONRANGEOPTIONS._serialized_end=1391 _FIELDDESCRIPTORPROTO._serialized_start=1394 _FIELDDESCRIPTORPROTO._serialized_end=2119 _FIELDDESCRIPTORPROTO_TYPE._serialized_start=1740 _FIELDDESCRIPTORPROTO_TYPE._serialized_end=2050 _FIELDDESCRIPTORPROTO_LABEL._serialized_start=2052 _FIELDDESCRIPTORPROTO_LABEL._serialized_end=2119 _ONEOFDESCRIPTORPROTO._serialized_start=2121 _ONEOFDESCRIPTORPROTO._serialized_end=2205 _ENUMDESCRIPTORPROTO._serialized_start=2208 _ENUMDESCRIPTORPROTO._serialized_end=2500 _ENUMDESCRIPTORPROTO_ENUMRESERVEDRANGE._serialized_start=2453 _ENUMDESCRIPTORPROTO_ENUMRESERVEDRANGE._serialized_end=2500 _ENUMVALUEDESCRIPTORPROTO._serialized_start=2502 _ENUMVALUEDESCRIPTORPROTO._serialized_end=2610 _SERVICEDESCRIPTORPROTO._serialized_start=2613 _SERVICEDESCRIPTORPROTO._serialized_end=2757 _METHODDESCRIPTORPROTO._serialized_start=2760 _METHODDESCRIPTORPROTO._serialized_end=2953 _FILEOPTIONS._serialized_start=2956 _FILEOPTIONS._serialized_end=3761 _FILEOPTIONS_OPTIMIZEMODE._serialized_start=3686 _FILEOPTIONS_OPTIMIZEMODE._serialized_end=3744 _MESSAGEOPTIONS._serialized_start=3764 _MESSAGEOPTIONS._serialized_end=4024 _FIELDOPTIONS._serialized_start=4027 _FIELDOPTIONS._serialized_end=4473 _FIELDOPTIONS_CTYPE._serialized_start=4354 _FIELDOPTIONS_CTYPE._serialized_end=4401 _FIELDOPTIONS_JSTYPE._serialized_start=4403 _FIELDOPTIONS_JSTYPE._serialized_end=4456 _ONEOFOPTIONS._serialized_start=4475 _ONEOFOPTIONS._serialized_end=4569 _ENUMOPTIONS._serialized_start=4572 _ENUMOPTIONS._serialized_end=4719 _ENUMVALUEOPTIONS._serialized_start=4721 _ENUMVALUEOPTIONS._serialized_end=4846 _SERVICEOPTIONS._serialized_start=4848 _SERVICEOPTIONS._serialized_end=4971 _METHODOPTIONS._serialized_start=4974 _METHODOPTIONS._serialized_end=5275 _METHODOPTIONS_IDEMPOTENCYLEVEL._serialized_start=5184 _METHODOPTIONS_IDEMPOTENCYLEVEL._serialized_end=5264 _UNINTERPRETEDOPTION._serialized_start=5278 _UNINTERPRETEDOPTION._serialized_end=5564 _UNINTERPRETEDOPTION_NAMEPART._serialized_start=5513 _UNINTERPRETEDOPTION_NAMEPART._serialized_end=5564 _SOURCECODEINFO._serialized_start=5567 _SOURCECODEINFO._serialized_end=5780 _SOURCECODEINFO_LOCATION._serialized_start=5646 _SOURCECODEINFO_LOCATION._serialized_end=5780 _GENERATEDCODEINFO._serialized_start=5783 _GENERATEDCODEINFO._serialized_end=5950 _GENERATEDCODEINFO_ANNOTATION._serialized_start=5871 _GENERATEDCODEINFO_ANNOTATION._serialized_end=5950 # @@protoc_insertion_point(module_scope) ================================================ FILE: openpype/hosts/nuke/vendor/google/protobuf/descriptor_pool.py ================================================ # Protocol Buffers - Google's data interchange format # Copyright 2008 Google Inc. All rights reserved. # https://developers.google.com/protocol-buffers/ # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are # met: # # * Redistributions of source code must retain the above copyright # notice, this list of conditions and the following disclaimer. # * Redistributions in binary form must reproduce the above # copyright notice, this list of conditions and the following disclaimer # in the documentation and/or other materials provided with the # distribution. # * Neither the name of Google Inc. nor the names of its # contributors may be used to endorse or promote products derived from # this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. """Provides DescriptorPool to use as a container for proto2 descriptors. The DescriptorPool is used in conjection with a DescriptorDatabase to maintain a collection of protocol buffer descriptors for use when dynamically creating message types at runtime. For most applications protocol buffers should be used via modules generated by the protocol buffer compiler tool. This should only be used when the type of protocol buffers used in an application or library cannot be predetermined. Below is a straightforward example on how to use this class:: pool = DescriptorPool() file_descriptor_protos = [ ... ] for file_descriptor_proto in file_descriptor_protos: pool.Add(file_descriptor_proto) my_message_descriptor = pool.FindMessageTypeByName('some.package.MessageType') The message descriptor can be used in conjunction with the message_factory module in order to create a protocol buffer class that can be encoded and decoded. If you want to get a Python class for the specified proto, use the helper functions inside google.protobuf.message_factory directly instead of this class. """ __author__ = 'matthewtoia@google.com (Matt Toia)' import collections import warnings from google.protobuf import descriptor from google.protobuf import descriptor_database from google.protobuf import text_encoding _USE_C_DESCRIPTORS = descriptor._USE_C_DESCRIPTORS # pylint: disable=protected-access def _Deprecated(func): """Mark functions as deprecated.""" def NewFunc(*args, **kwargs): warnings.warn( 'Call to deprecated function %s(). Note: Do add unlinked descriptors ' 'to descriptor_pool is wrong. Use Add() or AddSerializedFile() ' 'instead.' % func.__name__, category=DeprecationWarning) return func(*args, **kwargs) NewFunc.__name__ = func.__name__ NewFunc.__doc__ = func.__doc__ NewFunc.__dict__.update(func.__dict__) return NewFunc def _NormalizeFullyQualifiedName(name): """Remove leading period from fully-qualified type name. Due to b/13860351 in descriptor_database.py, types in the root namespace are generated with a leading period. This function removes that prefix. Args: name (str): The fully-qualified symbol name. Returns: str: The normalized fully-qualified symbol name. """ return name.lstrip('.') def _OptionsOrNone(descriptor_proto): """Returns the value of the field `options`, or None if it is not set.""" if descriptor_proto.HasField('options'): return descriptor_proto.options else: return None def _IsMessageSetExtension(field): return (field.is_extension and field.containing_type.has_options and field.containing_type.GetOptions().message_set_wire_format and field.type == descriptor.FieldDescriptor.TYPE_MESSAGE and field.label == descriptor.FieldDescriptor.LABEL_OPTIONAL) class DescriptorPool(object): """A collection of protobufs dynamically constructed by descriptor protos.""" if _USE_C_DESCRIPTORS: def __new__(cls, descriptor_db=None): # pylint: disable=protected-access return descriptor._message.DescriptorPool(descriptor_db) def __init__(self, descriptor_db=None): """Initializes a Pool of proto buffs. The descriptor_db argument to the constructor is provided to allow specialized file descriptor proto lookup code to be triggered on demand. An example would be an implementation which will read and compile a file specified in a call to FindFileByName() and not require the call to Add() at all. Results from this database will be cached internally here as well. Args: descriptor_db: A secondary source of file descriptors. """ self._internal_db = descriptor_database.DescriptorDatabase() self._descriptor_db = descriptor_db self._descriptors = {} self._enum_descriptors = {} self._service_descriptors = {} self._file_descriptors = {} self._toplevel_extensions = {} # TODO(jieluo): Remove _file_desc_by_toplevel_extension after # maybe year 2020 for compatibility issue (with 3.4.1 only). self._file_desc_by_toplevel_extension = {} self._top_enum_values = {} # We store extensions in two two-level mappings: The first key is the # descriptor of the message being extended, the second key is the extension # full name or its tag number. self._extensions_by_name = collections.defaultdict(dict) self._extensions_by_number = collections.defaultdict(dict) def _CheckConflictRegister(self, desc, desc_name, file_name): """Check if the descriptor name conflicts with another of the same name. Args: desc: Descriptor of a message, enum, service, extension or enum value. desc_name (str): the full name of desc. file_name (str): The file name of descriptor. """ for register, descriptor_type in [ (self._descriptors, descriptor.Descriptor), (self._enum_descriptors, descriptor.EnumDescriptor), (self._service_descriptors, descriptor.ServiceDescriptor), (self._toplevel_extensions, descriptor.FieldDescriptor), (self._top_enum_values, descriptor.EnumValueDescriptor)]: if desc_name in register: old_desc = register[desc_name] if isinstance(old_desc, descriptor.EnumValueDescriptor): old_file = old_desc.type.file.name else: old_file = old_desc.file.name if not isinstance(desc, descriptor_type) or ( old_file != file_name): error_msg = ('Conflict register for file "' + file_name + '": ' + desc_name + ' is already defined in file "' + old_file + '". Please fix the conflict by adding ' 'package name on the proto file, or use different ' 'name for the duplication.') if isinstance(desc, descriptor.EnumValueDescriptor): error_msg += ('\nNote: enum values appear as ' 'siblings of the enum type instead of ' 'children of it.') raise TypeError(error_msg) return def Add(self, file_desc_proto): """Adds the FileDescriptorProto and its types to this pool. Args: file_desc_proto (FileDescriptorProto): The file descriptor to add. """ self._internal_db.Add(file_desc_proto) def AddSerializedFile(self, serialized_file_desc_proto): """Adds the FileDescriptorProto and its types to this pool. Args: serialized_file_desc_proto (bytes): A bytes string, serialization of the :class:`FileDescriptorProto` to add. Returns: FileDescriptor: Descriptor for the added file. """ # pylint: disable=g-import-not-at-top from google.protobuf import descriptor_pb2 file_desc_proto = descriptor_pb2.FileDescriptorProto.FromString( serialized_file_desc_proto) file_desc = self._ConvertFileProtoToFileDescriptor(file_desc_proto) file_desc.serialized_pb = serialized_file_desc_proto return file_desc # Add Descriptor to descriptor pool is dreprecated. Please use Add() # or AddSerializedFile() to add a FileDescriptorProto instead. @_Deprecated def AddDescriptor(self, desc): self._AddDescriptor(desc) # Never call this method. It is for internal usage only. def _AddDescriptor(self, desc): """Adds a Descriptor to the pool, non-recursively. If the Descriptor contains nested messages or enums, the caller must explicitly register them. This method also registers the FileDescriptor associated with the message. Args: desc: A Descriptor. """ if not isinstance(desc, descriptor.Descriptor): raise TypeError('Expected instance of descriptor.Descriptor.') self._CheckConflictRegister(desc, desc.full_name, desc.file.name) self._descriptors[desc.full_name] = desc self._AddFileDescriptor(desc.file) # Add EnumDescriptor to descriptor pool is dreprecated. Please use Add() # or AddSerializedFile() to add a FileDescriptorProto instead. @_Deprecated def AddEnumDescriptor(self, enum_desc): self._AddEnumDescriptor(enum_desc) # Never call this method. It is for internal usage only. def _AddEnumDescriptor(self, enum_desc): """Adds an EnumDescriptor to the pool. This method also registers the FileDescriptor associated with the enum. Args: enum_desc: An EnumDescriptor. """ if not isinstance(enum_desc, descriptor.EnumDescriptor): raise TypeError('Expected instance of descriptor.EnumDescriptor.') file_name = enum_desc.file.name self._CheckConflictRegister(enum_desc, enum_desc.full_name, file_name) self._enum_descriptors[enum_desc.full_name] = enum_desc # Top enum values need to be indexed. # Count the number of dots to see whether the enum is toplevel or nested # in a message. We cannot use enum_desc.containing_type at this stage. if enum_desc.file.package: top_level = (enum_desc.full_name.count('.') - enum_desc.file.package.count('.') == 1) else: top_level = enum_desc.full_name.count('.') == 0 if top_level: file_name = enum_desc.file.name package = enum_desc.file.package for enum_value in enum_desc.values: full_name = _NormalizeFullyQualifiedName( '.'.join((package, enum_value.name))) self._CheckConflictRegister(enum_value, full_name, file_name) self._top_enum_values[full_name] = enum_value self._AddFileDescriptor(enum_desc.file) # Add ServiceDescriptor to descriptor pool is dreprecated. Please use Add() # or AddSerializedFile() to add a FileDescriptorProto instead. @_Deprecated def AddServiceDescriptor(self, service_desc): self._AddServiceDescriptor(service_desc) # Never call this method. It is for internal usage only. def _AddServiceDescriptor(self, service_desc): """Adds a ServiceDescriptor to the pool. Args: service_desc: A ServiceDescriptor. """ if not isinstance(service_desc, descriptor.ServiceDescriptor): raise TypeError('Expected instance of descriptor.ServiceDescriptor.') self._CheckConflictRegister(service_desc, service_desc.full_name, service_desc.file.name) self._service_descriptors[service_desc.full_name] = service_desc # Add ExtensionDescriptor to descriptor pool is dreprecated. Please use Add() # or AddSerializedFile() to add a FileDescriptorProto instead. @_Deprecated def AddExtensionDescriptor(self, extension): self._AddExtensionDescriptor(extension) # Never call this method. It is for internal usage only. def _AddExtensionDescriptor(self, extension): """Adds a FieldDescriptor describing an extension to the pool. Args: extension: A FieldDescriptor. Raises: AssertionError: when another extension with the same number extends the same message. TypeError: when the specified extension is not a descriptor.FieldDescriptor. """ if not (isinstance(extension, descriptor.FieldDescriptor) and extension.is_extension): raise TypeError('Expected an extension descriptor.') if extension.extension_scope is None: self._toplevel_extensions[extension.full_name] = extension try: existing_desc = self._extensions_by_number[ extension.containing_type][extension.number] except KeyError: pass else: if extension is not existing_desc: raise AssertionError( 'Extensions "%s" and "%s" both try to extend message type "%s" ' 'with field number %d.' % (extension.full_name, existing_desc.full_name, extension.containing_type.full_name, extension.number)) self._extensions_by_number[extension.containing_type][ extension.number] = extension self._extensions_by_name[extension.containing_type][ extension.full_name] = extension # Also register MessageSet extensions with the type name. if _IsMessageSetExtension(extension): self._extensions_by_name[extension.containing_type][ extension.message_type.full_name] = extension @_Deprecated def AddFileDescriptor(self, file_desc): self._InternalAddFileDescriptor(file_desc) # Never call this method. It is for internal usage only. def _InternalAddFileDescriptor(self, file_desc): """Adds a FileDescriptor to the pool, non-recursively. If the FileDescriptor contains messages or enums, the caller must explicitly register them. Args: file_desc: A FileDescriptor. """ self._AddFileDescriptor(file_desc) # TODO(jieluo): This is a temporary solution for FieldDescriptor.file. # FieldDescriptor.file is added in code gen. Remove this solution after # maybe 2020 for compatibility reason (with 3.4.1 only). for extension in file_desc.extensions_by_name.values(): self._file_desc_by_toplevel_extension[ extension.full_name] = file_desc def _AddFileDescriptor(self, file_desc): """Adds a FileDescriptor to the pool, non-recursively. If the FileDescriptor contains messages or enums, the caller must explicitly register them. Args: file_desc: A FileDescriptor. """ if not isinstance(file_desc, descriptor.FileDescriptor): raise TypeError('Expected instance of descriptor.FileDescriptor.') self._file_descriptors[file_desc.name] = file_desc def FindFileByName(self, file_name): """Gets a FileDescriptor by file name. Args: file_name (str): The path to the file to get a descriptor for. Returns: FileDescriptor: The descriptor for the named file. Raises: KeyError: if the file cannot be found in the pool. """ try: return self._file_descriptors[file_name] except KeyError: pass try: file_proto = self._internal_db.FindFileByName(file_name) except KeyError as error: if self._descriptor_db: file_proto = self._descriptor_db.FindFileByName(file_name) else: raise error if not file_proto: raise KeyError('Cannot find a file named %s' % file_name) return self._ConvertFileProtoToFileDescriptor(file_proto) def FindFileContainingSymbol(self, symbol): """Gets the FileDescriptor for the file containing the specified symbol. Args: symbol (str): The name of the symbol to search for. Returns: FileDescriptor: Descriptor for the file that contains the specified symbol. Raises: KeyError: if the file cannot be found in the pool. """ symbol = _NormalizeFullyQualifiedName(symbol) try: return self._InternalFindFileContainingSymbol(symbol) except KeyError: pass try: # Try fallback database. Build and find again if possible. self._FindFileContainingSymbolInDb(symbol) return self._InternalFindFileContainingSymbol(symbol) except KeyError: raise KeyError('Cannot find a file containing %s' % symbol) def _InternalFindFileContainingSymbol(self, symbol): """Gets the already built FileDescriptor containing the specified symbol. Args: symbol (str): The name of the symbol to search for. Returns: FileDescriptor: Descriptor for the file that contains the specified symbol. Raises: KeyError: if the file cannot be found in the pool. """ try: return self._descriptors[symbol].file except KeyError: pass try: return self._enum_descriptors[symbol].file except KeyError: pass try: return self._service_descriptors[symbol].file except KeyError: pass try: return self._top_enum_values[symbol].type.file except KeyError: pass try: return self._file_desc_by_toplevel_extension[symbol] except KeyError: pass # Try fields, enum values and nested extensions inside a message. top_name, _, sub_name = symbol.rpartition('.') try: message = self.FindMessageTypeByName(top_name) assert (sub_name in message.extensions_by_name or sub_name in message.fields_by_name or sub_name in message.enum_values_by_name) return message.file except (KeyError, AssertionError): raise KeyError('Cannot find a file containing %s' % symbol) def FindMessageTypeByName(self, full_name): """Loads the named descriptor from the pool. Args: full_name (str): The full name of the descriptor to load. Returns: Descriptor: The descriptor for the named type. Raises: KeyError: if the message cannot be found in the pool. """ full_name = _NormalizeFullyQualifiedName(full_name) if full_name not in self._descriptors: self._FindFileContainingSymbolInDb(full_name) return self._descriptors[full_name] def FindEnumTypeByName(self, full_name): """Loads the named enum descriptor from the pool. Args: full_name (str): The full name of the enum descriptor to load. Returns: EnumDescriptor: The enum descriptor for the named type. Raises: KeyError: if the enum cannot be found in the pool. """ full_name = _NormalizeFullyQualifiedName(full_name) if full_name not in self._enum_descriptors: self._FindFileContainingSymbolInDb(full_name) return self._enum_descriptors[full_name] def FindFieldByName(self, full_name): """Loads the named field descriptor from the pool. Args: full_name (str): The full name of the field descriptor to load. Returns: FieldDescriptor: The field descriptor for the named field. Raises: KeyError: if the field cannot be found in the pool. """ full_name = _NormalizeFullyQualifiedName(full_name) message_name, _, field_name = full_name.rpartition('.') message_descriptor = self.FindMessageTypeByName(message_name) return message_descriptor.fields_by_name[field_name] def FindOneofByName(self, full_name): """Loads the named oneof descriptor from the pool. Args: full_name (str): The full name of the oneof descriptor to load. Returns: OneofDescriptor: The oneof descriptor for the named oneof. Raises: KeyError: if the oneof cannot be found in the pool. """ full_name = _NormalizeFullyQualifiedName(full_name) message_name, _, oneof_name = full_name.rpartition('.') message_descriptor = self.FindMessageTypeByName(message_name) return message_descriptor.oneofs_by_name[oneof_name] def FindExtensionByName(self, full_name): """Loads the named extension descriptor from the pool. Args: full_name (str): The full name of the extension descriptor to load. Returns: FieldDescriptor: The field descriptor for the named extension. Raises: KeyError: if the extension cannot be found in the pool. """ full_name = _NormalizeFullyQualifiedName(full_name) try: # The proto compiler does not give any link between the FileDescriptor # and top-level extensions unless the FileDescriptorProto is added to # the DescriptorDatabase, but this can impact memory usage. # So we registered these extensions by name explicitly. return self._toplevel_extensions[full_name] except KeyError: pass message_name, _, extension_name = full_name.rpartition('.') try: # Most extensions are nested inside a message. scope = self.FindMessageTypeByName(message_name) except KeyError: # Some extensions are defined at file scope. scope = self._FindFileContainingSymbolInDb(full_name) return scope.extensions_by_name[extension_name] def FindExtensionByNumber(self, message_descriptor, number): """Gets the extension of the specified message with the specified number. Extensions have to be registered to this pool by calling :func:`Add` or :func:`AddExtensionDescriptor`. Args: message_descriptor (Descriptor): descriptor of the extended message. number (int): Number of the extension field. Returns: FieldDescriptor: The descriptor for the extension. Raises: KeyError: when no extension with the given number is known for the specified message. """ try: return self._extensions_by_number[message_descriptor][number] except KeyError: self._TryLoadExtensionFromDB(message_descriptor, number) return self._extensions_by_number[message_descriptor][number] def FindAllExtensions(self, message_descriptor): """Gets all the known extensions of a given message. Extensions have to be registered to this pool by build related :func:`Add` or :func:`AddExtensionDescriptor`. Args: message_descriptor (Descriptor): Descriptor of the extended message. Returns: list[FieldDescriptor]: Field descriptors describing the extensions. """ # Fallback to descriptor db if FindAllExtensionNumbers is provided. if self._descriptor_db and hasattr( self._descriptor_db, 'FindAllExtensionNumbers'): full_name = message_descriptor.full_name all_numbers = self._descriptor_db.FindAllExtensionNumbers(full_name) for number in all_numbers: if number in self._extensions_by_number[message_descriptor]: continue self._TryLoadExtensionFromDB(message_descriptor, number) return list(self._extensions_by_number[message_descriptor].values()) def _TryLoadExtensionFromDB(self, message_descriptor, number): """Try to Load extensions from descriptor db. Args: message_descriptor: descriptor of the extended message. number: the extension number that needs to be loaded. """ if not self._descriptor_db: return # Only supported when FindFileContainingExtension is provided. if not hasattr( self._descriptor_db, 'FindFileContainingExtension'): return full_name = message_descriptor.full_name file_proto = self._descriptor_db.FindFileContainingExtension( full_name, number) if file_proto is None: return try: self._ConvertFileProtoToFileDescriptor(file_proto) except: warn_msg = ('Unable to load proto file %s for extension number %d.' % (file_proto.name, number)) warnings.warn(warn_msg, RuntimeWarning) def FindServiceByName(self, full_name): """Loads the named service descriptor from the pool. Args: full_name (str): The full name of the service descriptor to load. Returns: ServiceDescriptor: The service descriptor for the named service. Raises: KeyError: if the service cannot be found in the pool. """ full_name = _NormalizeFullyQualifiedName(full_name) if full_name not in self._service_descriptors: self._FindFileContainingSymbolInDb(full_name) return self._service_descriptors[full_name] def FindMethodByName(self, full_name): """Loads the named service method descriptor from the pool. Args: full_name (str): The full name of the method descriptor to load. Returns: MethodDescriptor: The method descriptor for the service method. Raises: KeyError: if the method cannot be found in the pool. """ full_name = _NormalizeFullyQualifiedName(full_name) service_name, _, method_name = full_name.rpartition('.') service_descriptor = self.FindServiceByName(service_name) return service_descriptor.methods_by_name[method_name] def _FindFileContainingSymbolInDb(self, symbol): """Finds the file in descriptor DB containing the specified symbol. Args: symbol (str): The name of the symbol to search for. Returns: FileDescriptor: The file that contains the specified symbol. Raises: KeyError: if the file cannot be found in the descriptor database. """ try: file_proto = self._internal_db.FindFileContainingSymbol(symbol) except KeyError as error: if self._descriptor_db: file_proto = self._descriptor_db.FindFileContainingSymbol(symbol) else: raise error if not file_proto: raise KeyError('Cannot find a file containing %s' % symbol) return self._ConvertFileProtoToFileDescriptor(file_proto) def _ConvertFileProtoToFileDescriptor(self, file_proto): """Creates a FileDescriptor from a proto or returns a cached copy. This method also has the side effect of loading all the symbols found in the file into the appropriate dictionaries in the pool. Args: file_proto: The proto to convert. Returns: A FileDescriptor matching the passed in proto. """ if file_proto.name not in self._file_descriptors: built_deps = list(self._GetDeps(file_proto.dependency)) direct_deps = [self.FindFileByName(n) for n in file_proto.dependency] public_deps = [direct_deps[i] for i in file_proto.public_dependency] file_descriptor = descriptor.FileDescriptor( pool=self, name=file_proto.name, package=file_proto.package, syntax=file_proto.syntax, options=_OptionsOrNone(file_proto), serialized_pb=file_proto.SerializeToString(), dependencies=direct_deps, public_dependencies=public_deps, # pylint: disable=protected-access create_key=descriptor._internal_create_key) scope = {} # This loop extracts all the message and enum types from all the # dependencies of the file_proto. This is necessary to create the # scope of available message types when defining the passed in # file proto. for dependency in built_deps: scope.update(self._ExtractSymbols( dependency.message_types_by_name.values())) scope.update((_PrefixWithDot(enum.full_name), enum) for enum in dependency.enum_types_by_name.values()) for message_type in file_proto.message_type: message_desc = self._ConvertMessageDescriptor( message_type, file_proto.package, file_descriptor, scope, file_proto.syntax) file_descriptor.message_types_by_name[message_desc.name] = ( message_desc) for enum_type in file_proto.enum_type: file_descriptor.enum_types_by_name[enum_type.name] = ( self._ConvertEnumDescriptor(enum_type, file_proto.package, file_descriptor, None, scope, True)) for index, extension_proto in enumerate(file_proto.extension): extension_desc = self._MakeFieldDescriptor( extension_proto, file_proto.package, index, file_descriptor, is_extension=True) extension_desc.containing_type = self._GetTypeFromScope( file_descriptor.package, extension_proto.extendee, scope) self._SetFieldType(extension_proto, extension_desc, file_descriptor.package, scope) file_descriptor.extensions_by_name[extension_desc.name] = ( extension_desc) self._file_desc_by_toplevel_extension[extension_desc.full_name] = ( file_descriptor) for desc_proto in file_proto.message_type: self._SetAllFieldTypes(file_proto.package, desc_proto, scope) if file_proto.package: desc_proto_prefix = _PrefixWithDot(file_proto.package) else: desc_proto_prefix = '' for desc_proto in file_proto.message_type: desc = self._GetTypeFromScope( desc_proto_prefix, desc_proto.name, scope) file_descriptor.message_types_by_name[desc_proto.name] = desc for index, service_proto in enumerate(file_proto.service): file_descriptor.services_by_name[service_proto.name] = ( self._MakeServiceDescriptor(service_proto, index, scope, file_proto.package, file_descriptor)) self._file_descriptors[file_proto.name] = file_descriptor # Add extensions to the pool file_desc = self._file_descriptors[file_proto.name] for extension in file_desc.extensions_by_name.values(): self._AddExtensionDescriptor(extension) for message_type in file_desc.message_types_by_name.values(): for extension in message_type.extensions: self._AddExtensionDescriptor(extension) return file_desc def _ConvertMessageDescriptor(self, desc_proto, package=None, file_desc=None, scope=None, syntax=None): """Adds the proto to the pool in the specified package. Args: desc_proto: The descriptor_pb2.DescriptorProto protobuf message. package: The package the proto should be located in. file_desc: The file containing this message. scope: Dict mapping short and full symbols to message and enum types. syntax: string indicating syntax of the file ("proto2" or "proto3") Returns: The added descriptor. """ if package: desc_name = '.'.join((package, desc_proto.name)) else: desc_name = desc_proto.name if file_desc is None: file_name = None else: file_name = file_desc.name if scope is None: scope = {} nested = [ self._ConvertMessageDescriptor( nested, desc_name, file_desc, scope, syntax) for nested in desc_proto.nested_type] enums = [ self._ConvertEnumDescriptor(enum, desc_name, file_desc, None, scope, False) for enum in desc_proto.enum_type] fields = [self._MakeFieldDescriptor(field, desc_name, index, file_desc) for index, field in enumerate(desc_proto.field)] extensions = [ self._MakeFieldDescriptor(extension, desc_name, index, file_desc, is_extension=True) for index, extension in enumerate(desc_proto.extension)] oneofs = [ # pylint: disable=g-complex-comprehension descriptor.OneofDescriptor( desc.name, '.'.join((desc_name, desc.name)), index, None, [], _OptionsOrNone(desc), # pylint: disable=protected-access create_key=descriptor._internal_create_key) for index, desc in enumerate(desc_proto.oneof_decl) ] extension_ranges = [(r.start, r.end) for r in desc_proto.extension_range] if extension_ranges: is_extendable = True else: is_extendable = False desc = descriptor.Descriptor( name=desc_proto.name, full_name=desc_name, filename=file_name, containing_type=None, fields=fields, oneofs=oneofs, nested_types=nested, enum_types=enums, extensions=extensions, options=_OptionsOrNone(desc_proto), is_extendable=is_extendable, extension_ranges=extension_ranges, file=file_desc, serialized_start=None, serialized_end=None, syntax=syntax, # pylint: disable=protected-access create_key=descriptor._internal_create_key) for nested in desc.nested_types: nested.containing_type = desc for enum in desc.enum_types: enum.containing_type = desc for field_index, field_desc in enumerate(desc_proto.field): if field_desc.HasField('oneof_index'): oneof_index = field_desc.oneof_index oneofs[oneof_index].fields.append(fields[field_index]) fields[field_index].containing_oneof = oneofs[oneof_index] scope[_PrefixWithDot(desc_name)] = desc self._CheckConflictRegister(desc, desc.full_name, desc.file.name) self._descriptors[desc_name] = desc return desc def _ConvertEnumDescriptor(self, enum_proto, package=None, file_desc=None, containing_type=None, scope=None, top_level=False): """Make a protobuf EnumDescriptor given an EnumDescriptorProto protobuf. Args: enum_proto: The descriptor_pb2.EnumDescriptorProto protobuf message. package: Optional package name for the new message EnumDescriptor. file_desc: The file containing the enum descriptor. containing_type: The type containing this enum. scope: Scope containing available types. top_level: If True, the enum is a top level symbol. If False, the enum is defined inside a message. Returns: The added descriptor """ if package: enum_name = '.'.join((package, enum_proto.name)) else: enum_name = enum_proto.name if file_desc is None: file_name = None else: file_name = file_desc.name values = [self._MakeEnumValueDescriptor(value, index) for index, value in enumerate(enum_proto.value)] desc = descriptor.EnumDescriptor(name=enum_proto.name, full_name=enum_name, filename=file_name, file=file_desc, values=values, containing_type=containing_type, options=_OptionsOrNone(enum_proto), # pylint: disable=protected-access create_key=descriptor._internal_create_key) scope['.%s' % enum_name] = desc self._CheckConflictRegister(desc, desc.full_name, desc.file.name) self._enum_descriptors[enum_name] = desc # Add top level enum values. if top_level: for value in values: full_name = _NormalizeFullyQualifiedName( '.'.join((package, value.name))) self._CheckConflictRegister(value, full_name, file_name) self._top_enum_values[full_name] = value return desc def _MakeFieldDescriptor(self, field_proto, message_name, index, file_desc, is_extension=False): """Creates a field descriptor from a FieldDescriptorProto. For message and enum type fields, this method will do a look up in the pool for the appropriate descriptor for that type. If it is unavailable, it will fall back to the _source function to create it. If this type is still unavailable, construction will fail. Args: field_proto: The proto describing the field. message_name: The name of the containing message. index: Index of the field file_desc: The file containing the field descriptor. is_extension: Indication that this field is for an extension. Returns: An initialized FieldDescriptor object """ if message_name: full_name = '.'.join((message_name, field_proto.name)) else: full_name = field_proto.name if field_proto.json_name: json_name = field_proto.json_name else: json_name = None return descriptor.FieldDescriptor( name=field_proto.name, full_name=full_name, index=index, number=field_proto.number, type=field_proto.type, cpp_type=None, message_type=None, enum_type=None, containing_type=None, label=field_proto.label, has_default_value=False, default_value=None, is_extension=is_extension, extension_scope=None, options=_OptionsOrNone(field_proto), json_name=json_name, file=file_desc, # pylint: disable=protected-access create_key=descriptor._internal_create_key) def _SetAllFieldTypes(self, package, desc_proto, scope): """Sets all the descriptor's fields's types. This method also sets the containing types on any extensions. Args: package: The current package of desc_proto. desc_proto: The message descriptor to update. scope: Enclosing scope of available types. """ package = _PrefixWithDot(package) main_desc = self._GetTypeFromScope(package, desc_proto.name, scope) if package == '.': nested_package = _PrefixWithDot(desc_proto.name) else: nested_package = '.'.join([package, desc_proto.name]) for field_proto, field_desc in zip(desc_proto.field, main_desc.fields): self._SetFieldType(field_proto, field_desc, nested_package, scope) for extension_proto, extension_desc in ( zip(desc_proto.extension, main_desc.extensions)): extension_desc.containing_type = self._GetTypeFromScope( nested_package, extension_proto.extendee, scope) self._SetFieldType(extension_proto, extension_desc, nested_package, scope) for nested_type in desc_proto.nested_type: self._SetAllFieldTypes(nested_package, nested_type, scope) def _SetFieldType(self, field_proto, field_desc, package, scope): """Sets the field's type, cpp_type, message_type and enum_type. Args: field_proto: Data about the field in proto format. field_desc: The descriptor to modify. package: The package the field's container is in. scope: Enclosing scope of available types. """ if field_proto.type_name: desc = self._GetTypeFromScope(package, field_proto.type_name, scope) else: desc = None if not field_proto.HasField('type'): if isinstance(desc, descriptor.Descriptor): field_proto.type = descriptor.FieldDescriptor.TYPE_MESSAGE else: field_proto.type = descriptor.FieldDescriptor.TYPE_ENUM field_desc.cpp_type = descriptor.FieldDescriptor.ProtoTypeToCppProtoType( field_proto.type) if (field_proto.type == descriptor.FieldDescriptor.TYPE_MESSAGE or field_proto.type == descriptor.FieldDescriptor.TYPE_GROUP): field_desc.message_type = desc if field_proto.type == descriptor.FieldDescriptor.TYPE_ENUM: field_desc.enum_type = desc if field_proto.label == descriptor.FieldDescriptor.LABEL_REPEATED: field_desc.has_default_value = False field_desc.default_value = [] elif field_proto.HasField('default_value'): field_desc.has_default_value = True if (field_proto.type == descriptor.FieldDescriptor.TYPE_DOUBLE or field_proto.type == descriptor.FieldDescriptor.TYPE_FLOAT): field_desc.default_value = float(field_proto.default_value) elif field_proto.type == descriptor.FieldDescriptor.TYPE_STRING: field_desc.default_value = field_proto.default_value elif field_proto.type == descriptor.FieldDescriptor.TYPE_BOOL: field_desc.default_value = field_proto.default_value.lower() == 'true' elif field_proto.type == descriptor.FieldDescriptor.TYPE_ENUM: field_desc.default_value = field_desc.enum_type.values_by_name[ field_proto.default_value].number elif field_proto.type == descriptor.FieldDescriptor.TYPE_BYTES: field_desc.default_value = text_encoding.CUnescape( field_proto.default_value) elif field_proto.type == descriptor.FieldDescriptor.TYPE_MESSAGE: field_desc.default_value = None else: # All other types are of the "int" type. field_desc.default_value = int(field_proto.default_value) else: field_desc.has_default_value = False if (field_proto.type == descriptor.FieldDescriptor.TYPE_DOUBLE or field_proto.type == descriptor.FieldDescriptor.TYPE_FLOAT): field_desc.default_value = 0.0 elif field_proto.type == descriptor.FieldDescriptor.TYPE_STRING: field_desc.default_value = u'' elif field_proto.type == descriptor.FieldDescriptor.TYPE_BOOL: field_desc.default_value = False elif field_proto.type == descriptor.FieldDescriptor.TYPE_ENUM: field_desc.default_value = field_desc.enum_type.values[0].number elif field_proto.type == descriptor.FieldDescriptor.TYPE_BYTES: field_desc.default_value = b'' elif field_proto.type == descriptor.FieldDescriptor.TYPE_MESSAGE: field_desc.default_value = None elif field_proto.type == descriptor.FieldDescriptor.TYPE_GROUP: field_desc.default_value = None else: # All other types are of the "int" type. field_desc.default_value = 0 field_desc.type = field_proto.type def _MakeEnumValueDescriptor(self, value_proto, index): """Creates a enum value descriptor object from a enum value proto. Args: value_proto: The proto describing the enum value. index: The index of the enum value. Returns: An initialized EnumValueDescriptor object. """ return descriptor.EnumValueDescriptor( name=value_proto.name, index=index, number=value_proto.number, options=_OptionsOrNone(value_proto), type=None, # pylint: disable=protected-access create_key=descriptor._internal_create_key) def _MakeServiceDescriptor(self, service_proto, service_index, scope, package, file_desc): """Make a protobuf ServiceDescriptor given a ServiceDescriptorProto. Args: service_proto: The descriptor_pb2.ServiceDescriptorProto protobuf message. service_index: The index of the service in the File. scope: Dict mapping short and full symbols to message and enum types. package: Optional package name for the new message EnumDescriptor. file_desc: The file containing the service descriptor. Returns: The added descriptor. """ if package: service_name = '.'.join((package, service_proto.name)) else: service_name = service_proto.name methods = [self._MakeMethodDescriptor(method_proto, service_name, package, scope, index) for index, method_proto in enumerate(service_proto.method)] desc = descriptor.ServiceDescriptor( name=service_proto.name, full_name=service_name, index=service_index, methods=methods, options=_OptionsOrNone(service_proto), file=file_desc, # pylint: disable=protected-access create_key=descriptor._internal_create_key) self._CheckConflictRegister(desc, desc.full_name, desc.file.name) self._service_descriptors[service_name] = desc return desc def _MakeMethodDescriptor(self, method_proto, service_name, package, scope, index): """Creates a method descriptor from a MethodDescriptorProto. Args: method_proto: The proto describing the method. service_name: The name of the containing service. package: Optional package name to look up for types. scope: Scope containing available types. index: Index of the method in the service. Returns: An initialized MethodDescriptor object. """ full_name = '.'.join((service_name, method_proto.name)) input_type = self._GetTypeFromScope( package, method_proto.input_type, scope) output_type = self._GetTypeFromScope( package, method_proto.output_type, scope) return descriptor.MethodDescriptor( name=method_proto.name, full_name=full_name, index=index, containing_service=None, input_type=input_type, output_type=output_type, client_streaming=method_proto.client_streaming, server_streaming=method_proto.server_streaming, options=_OptionsOrNone(method_proto), # pylint: disable=protected-access create_key=descriptor._internal_create_key) def _ExtractSymbols(self, descriptors): """Pulls out all the symbols from descriptor protos. Args: descriptors: The messages to extract descriptors from. Yields: A two element tuple of the type name and descriptor object. """ for desc in descriptors: yield (_PrefixWithDot(desc.full_name), desc) for symbol in self._ExtractSymbols(desc.nested_types): yield symbol for enum in desc.enum_types: yield (_PrefixWithDot(enum.full_name), enum) def _GetDeps(self, dependencies, visited=None): """Recursively finds dependencies for file protos. Args: dependencies: The names of the files being depended on. visited: The names of files already found. Yields: Each direct and indirect dependency. """ visited = visited or set() for dependency in dependencies: if dependency not in visited: visited.add(dependency) dep_desc = self.FindFileByName(dependency) yield dep_desc public_files = [d.name for d in dep_desc.public_dependencies] yield from self._GetDeps(public_files, visited) def _GetTypeFromScope(self, package, type_name, scope): """Finds a given type name in the current scope. Args: package: The package the proto should be located in. type_name: The name of the type to be found in the scope. scope: Dict mapping short and full symbols to message and enum types. Returns: The descriptor for the requested type. """ if type_name not in scope: components = _PrefixWithDot(package).split('.') while components: possible_match = '.'.join(components + [type_name]) if possible_match in scope: type_name = possible_match break else: components.pop(-1) return scope[type_name] def _PrefixWithDot(name): return name if name.startswith('.') else '.%s' % name if _USE_C_DESCRIPTORS: # TODO(amauryfa): This pool could be constructed from Python code, when we # support a flag like 'use_cpp_generated_pool=True'. # pylint: disable=protected-access _DEFAULT = descriptor._message.default_pool else: _DEFAULT = DescriptorPool() def Default(): return _DEFAULT ================================================ FILE: openpype/hosts/nuke/vendor/google/protobuf/duration_pb2.py ================================================ # -*- coding: utf-8 -*- # Generated by the protocol buffer compiler. DO NOT EDIT! # source: google/protobuf/duration.proto """Generated protocol buffer code.""" from google.protobuf.internal import builder as _builder from google.protobuf import descriptor as _descriptor from google.protobuf import descriptor_pool as _descriptor_pool from google.protobuf import symbol_database as _symbol_database # @@protoc_insertion_point(imports) _sym_db = _symbol_database.Default() DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x1egoogle/protobuf/duration.proto\x12\x0fgoogle.protobuf\"*\n\x08\x44uration\x12\x0f\n\x07seconds\x18\x01 \x01(\x03\x12\r\n\x05nanos\x18\x02 \x01(\x05\x42\x83\x01\n\x13\x63om.google.protobufB\rDurationProtoP\x01Z1google.golang.org/protobuf/types/known/durationpb\xf8\x01\x01\xa2\x02\x03GPB\xaa\x02\x1eGoogle.Protobuf.WellKnownTypesb\x06proto3') _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals()) _builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'google.protobuf.duration_pb2', globals()) if _descriptor._USE_C_DESCRIPTORS == False: DESCRIPTOR._options = None DESCRIPTOR._serialized_options = b'\n\023com.google.protobufB\rDurationProtoP\001Z1google.golang.org/protobuf/types/known/durationpb\370\001\001\242\002\003GPB\252\002\036Google.Protobuf.WellKnownTypes' _DURATION._serialized_start=51 _DURATION._serialized_end=93 # @@protoc_insertion_point(module_scope) ================================================ FILE: openpype/hosts/nuke/vendor/google/protobuf/empty_pb2.py ================================================ # -*- coding: utf-8 -*- # Generated by the protocol buffer compiler. DO NOT EDIT! # source: google/protobuf/empty.proto """Generated protocol buffer code.""" from google.protobuf.internal import builder as _builder from google.protobuf import descriptor as _descriptor from google.protobuf import descriptor_pool as _descriptor_pool from google.protobuf import symbol_database as _symbol_database # @@protoc_insertion_point(imports) _sym_db = _symbol_database.Default() DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x1bgoogle/protobuf/empty.proto\x12\x0fgoogle.protobuf\"\x07\n\x05\x45mptyB}\n\x13\x63om.google.protobufB\nEmptyProtoP\x01Z.google.golang.org/protobuf/types/known/emptypb\xf8\x01\x01\xa2\x02\x03GPB\xaa\x02\x1eGoogle.Protobuf.WellKnownTypesb\x06proto3') _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals()) _builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'google.protobuf.empty_pb2', globals()) if _descriptor._USE_C_DESCRIPTORS == False: DESCRIPTOR._options = None DESCRIPTOR._serialized_options = b'\n\023com.google.protobufB\nEmptyProtoP\001Z.google.golang.org/protobuf/types/known/emptypb\370\001\001\242\002\003GPB\252\002\036Google.Protobuf.WellKnownTypes' _EMPTY._serialized_start=48 _EMPTY._serialized_end=55 # @@protoc_insertion_point(module_scope) ================================================ FILE: openpype/hosts/nuke/vendor/google/protobuf/field_mask_pb2.py ================================================ # -*- coding: utf-8 -*- # Generated by the protocol buffer compiler. DO NOT EDIT! # source: google/protobuf/field_mask.proto """Generated protocol buffer code.""" from google.protobuf.internal import builder as _builder from google.protobuf import descriptor as _descriptor from google.protobuf import descriptor_pool as _descriptor_pool from google.protobuf import symbol_database as _symbol_database # @@protoc_insertion_point(imports) _sym_db = _symbol_database.Default() DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n google/protobuf/field_mask.proto\x12\x0fgoogle.protobuf\"\x1a\n\tFieldMask\x12\r\n\x05paths\x18\x01 \x03(\tB\x85\x01\n\x13\x63om.google.protobufB\x0e\x46ieldMaskProtoP\x01Z2google.golang.org/protobuf/types/known/fieldmaskpb\xf8\x01\x01\xa2\x02\x03GPB\xaa\x02\x1eGoogle.Protobuf.WellKnownTypesb\x06proto3') _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals()) _builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'google.protobuf.field_mask_pb2', globals()) if _descriptor._USE_C_DESCRIPTORS == False: DESCRIPTOR._options = None DESCRIPTOR._serialized_options = b'\n\023com.google.protobufB\016FieldMaskProtoP\001Z2google.golang.org/protobuf/types/known/fieldmaskpb\370\001\001\242\002\003GPB\252\002\036Google.Protobuf.WellKnownTypes' _FIELDMASK._serialized_start=53 _FIELDMASK._serialized_end=79 # @@protoc_insertion_point(module_scope) ================================================ FILE: openpype/hosts/nuke/vendor/google/protobuf/internal/__init__.py ================================================ ================================================ FILE: openpype/hosts/nuke/vendor/google/protobuf/internal/_parameterized.py ================================================ #! /usr/bin/env python # # Protocol Buffers - Google's data interchange format # Copyright 2008 Google Inc. All rights reserved. # https://developers.google.com/protocol-buffers/ # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are # met: # # * Redistributions of source code must retain the above copyright # notice, this list of conditions and the following disclaimer. # * Redistributions in binary form must reproduce the above # copyright notice, this list of conditions and the following disclaimer # in the documentation and/or other materials provided with the # distribution. # * Neither the name of Google Inc. nor the names of its # contributors may be used to endorse or promote products derived from # this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. """Adds support for parameterized tests to Python's unittest TestCase class. A parameterized test is a method in a test case that is invoked with different argument tuples. A simple example: class AdditionExample(parameterized.TestCase): @parameterized.parameters( (1, 2, 3), (4, 5, 9), (1, 1, 3)) def testAddition(self, op1, op2, result): self.assertEqual(result, op1 + op2) Each invocation is a separate test case and properly isolated just like a normal test method, with its own setUp/tearDown cycle. In the example above, there are three separate testcases, one of which will fail due to an assertion error (1 + 1 != 3). Parameters for individual test cases can be tuples (with positional parameters) or dictionaries (with named parameters): class AdditionExample(parameterized.TestCase): @parameterized.parameters( {'op1': 1, 'op2': 2, 'result': 3}, {'op1': 4, 'op2': 5, 'result': 9}, ) def testAddition(self, op1, op2, result): self.assertEqual(result, op1 + op2) If a parameterized test fails, the error message will show the original test name (which is modified internally) and the arguments for the specific invocation, which are part of the string returned by the shortDescription() method on test cases. The id method of the test, used internally by the unittest framework, is also modified to show the arguments. To make sure that test names stay the same across several invocations, object representations like >>> class Foo(object): ... pass >>> repr(Foo()) '<__main__.Foo object at 0x23d8610>' are turned into '<__main__.Foo>'. For even more descriptive names, especially in test logs, you can use the named_parameters decorator. In this case, only tuples are supported, and the first parameters has to be a string (or an object that returns an apt name when converted via str()): class NamedExample(parameterized.TestCase): @parameterized.named_parameters( ('Normal', 'aa', 'aaa', True), ('EmptyPrefix', '', 'abc', True), ('BothEmpty', '', '', True)) def testStartsWith(self, prefix, string, result): self.assertEqual(result, strings.startswith(prefix)) Named tests also have the benefit that they can be run individually from the command line: $ testmodule.py NamedExample.testStartsWithNormal . -------------------------------------------------------------------- Ran 1 test in 0.000s OK Parameterized Classes ===================== If invocation arguments are shared across test methods in a single TestCase class, instead of decorating all test methods individually, the class itself can be decorated: @parameterized.parameters( (1, 2, 3) (4, 5, 9)) class ArithmeticTest(parameterized.TestCase): def testAdd(self, arg1, arg2, result): self.assertEqual(arg1 + arg2, result) def testSubtract(self, arg2, arg2, result): self.assertEqual(result - arg1, arg2) Inputs from Iterables ===================== If parameters should be shared across several test cases, or are dynamically created from other sources, a single non-tuple iterable can be passed into the decorator. This iterable will be used to obtain the test cases: class AdditionExample(parameterized.TestCase): @parameterized.parameters( c.op1, c.op2, c.result for c in testcases ) def testAddition(self, op1, op2, result): self.assertEqual(result, op1 + op2) Single-Argument Test Methods ============================ If a test method takes only one argument, the single argument does not need to be wrapped into a tuple: class NegativeNumberExample(parameterized.TestCase): @parameterized.parameters( -1, -3, -4, -5 ) def testIsNegative(self, arg): self.assertTrue(IsNegative(arg)) """ __author__ = 'tmarek@google.com (Torsten Marek)' import functools import re import types import unittest import uuid try: # Since python 3 import collections.abc as collections_abc except ImportError: # Won't work after python 3.8 import collections as collections_abc ADDR_RE = re.compile(r'\<([a-zA-Z0-9_\-\.]+) object at 0x[a-fA-F0-9]+\>') _SEPARATOR = uuid.uuid1().hex _FIRST_ARG = object() _ARGUMENT_REPR = object() def _CleanRepr(obj): return ADDR_RE.sub(r'<\1>', repr(obj)) # Helper function formerly from the unittest module, removed from it in # Python 2.7. def _StrClass(cls): return '%s.%s' % (cls.__module__, cls.__name__) def _NonStringIterable(obj): return (isinstance(obj, collections_abc.Iterable) and not isinstance(obj, str)) def _FormatParameterList(testcase_params): if isinstance(testcase_params, collections_abc.Mapping): return ', '.join('%s=%s' % (argname, _CleanRepr(value)) for argname, value in testcase_params.items()) elif _NonStringIterable(testcase_params): return ', '.join(map(_CleanRepr, testcase_params)) else: return _FormatParameterList((testcase_params,)) class _ParameterizedTestIter(object): """Callable and iterable class for producing new test cases.""" def __init__(self, test_method, testcases, naming_type): """Returns concrete test functions for a test and a list of parameters. The naming_type is used to determine the name of the concrete functions as reported by the unittest framework. If naming_type is _FIRST_ARG, the testcases must be tuples, and the first element must have a string representation that is a valid Python identifier. Args: test_method: The decorated test method. testcases: (list of tuple/dict) A list of parameter tuples/dicts for individual test invocations. naming_type: The test naming type, either _NAMED or _ARGUMENT_REPR. """ self._test_method = test_method self.testcases = testcases self._naming_type = naming_type def __call__(self, *args, **kwargs): raise RuntimeError('You appear to be running a parameterized test case ' 'without having inherited from parameterized.' 'TestCase. This is bad because none of ' 'your test cases are actually being run.') def __iter__(self): test_method = self._test_method naming_type = self._naming_type def MakeBoundParamTest(testcase_params): @functools.wraps(test_method) def BoundParamTest(self): if isinstance(testcase_params, collections_abc.Mapping): test_method(self, **testcase_params) elif _NonStringIterable(testcase_params): test_method(self, *testcase_params) else: test_method(self, testcase_params) if naming_type is _FIRST_ARG: # Signal the metaclass that the name of the test function is unique # and descriptive. BoundParamTest.__x_use_name__ = True BoundParamTest.__name__ += str(testcase_params[0]) testcase_params = testcase_params[1:] elif naming_type is _ARGUMENT_REPR: # __x_extra_id__ is used to pass naming information to the __new__ # method of TestGeneratorMetaclass. # The metaclass will make sure to create a unique, but nondescriptive # name for this test. BoundParamTest.__x_extra_id__ = '(%s)' % ( _FormatParameterList(testcase_params),) else: raise RuntimeError('%s is not a valid naming type.' % (naming_type,)) BoundParamTest.__doc__ = '%s(%s)' % ( BoundParamTest.__name__, _FormatParameterList(testcase_params)) if test_method.__doc__: BoundParamTest.__doc__ += '\n%s' % (test_method.__doc__,) return BoundParamTest return (MakeBoundParamTest(c) for c in self.testcases) def _IsSingletonList(testcases): """True iff testcases contains only a single non-tuple element.""" return len(testcases) == 1 and not isinstance(testcases[0], tuple) def _ModifyClass(class_object, testcases, naming_type): assert not getattr(class_object, '_id_suffix', None), ( 'Cannot add parameters to %s,' ' which already has parameterized methods.' % (class_object,)) class_object._id_suffix = id_suffix = {} # We change the size of __dict__ while we iterate over it, # which Python 3.x will complain about, so use copy(). for name, obj in class_object.__dict__.copy().items(): if (name.startswith(unittest.TestLoader.testMethodPrefix) and isinstance(obj, types.FunctionType)): delattr(class_object, name) methods = {} _UpdateClassDictForParamTestCase( methods, id_suffix, name, _ParameterizedTestIter(obj, testcases, naming_type)) for name, meth in methods.items(): setattr(class_object, name, meth) def _ParameterDecorator(naming_type, testcases): """Implementation of the parameterization decorators. Args: naming_type: The naming type. testcases: Testcase parameters. Returns: A function for modifying the decorated object. """ def _Apply(obj): if isinstance(obj, type): _ModifyClass( obj, list(testcases) if not isinstance(testcases, collections_abc.Sequence) else testcases, naming_type) return obj else: return _ParameterizedTestIter(obj, testcases, naming_type) if _IsSingletonList(testcases): assert _NonStringIterable(testcases[0]), ( 'Single parameter argument must be a non-string iterable') testcases = testcases[0] return _Apply def parameters(*testcases): # pylint: disable=invalid-name """A decorator for creating parameterized tests. See the module docstring for a usage example. Args: *testcases: Parameters for the decorated method, either a single iterable, or a list of tuples/dicts/objects (for tests with only one argument). Returns: A test generator to be handled by TestGeneratorMetaclass. """ return _ParameterDecorator(_ARGUMENT_REPR, testcases) def named_parameters(*testcases): # pylint: disable=invalid-name """A decorator for creating parameterized tests. See the module docstring for a usage example. The first element of each parameter tuple should be a string and will be appended to the name of the test method. Args: *testcases: Parameters for the decorated method, either a single iterable, or a list of tuples. Returns: A test generator to be handled by TestGeneratorMetaclass. """ return _ParameterDecorator(_FIRST_ARG, testcases) class TestGeneratorMetaclass(type): """Metaclass for test cases with test generators. A test generator is an iterable in a testcase that produces callables. These callables must be single-argument methods. These methods are injected into the class namespace and the original iterable is removed. If the name of the iterable conforms to the test pattern, the injected methods will be picked up as tests by the unittest framework. In general, it is supposed to be used in conjunction with the parameters decorator. """ def __new__(mcs, class_name, bases, dct): dct['_id_suffix'] = id_suffix = {} for name, obj in dct.copy().items(): if (name.startswith(unittest.TestLoader.testMethodPrefix) and _NonStringIterable(obj)): iterator = iter(obj) dct.pop(name) _UpdateClassDictForParamTestCase(dct, id_suffix, name, iterator) return type.__new__(mcs, class_name, bases, dct) def _UpdateClassDictForParamTestCase(dct, id_suffix, name, iterator): """Adds individual test cases to a dictionary. Args: dct: The target dictionary. id_suffix: The dictionary for mapping names to test IDs. name: The original name of the test case. iterator: The iterator generating the individual test cases. """ for idx, func in enumerate(iterator): assert callable(func), 'Test generators must yield callables, got %r' % ( func,) if getattr(func, '__x_use_name__', False): new_name = func.__name__ else: new_name = '%s%s%d' % (name, _SEPARATOR, idx) assert new_name not in dct, ( 'Name of parameterized test case "%s" not unique' % (new_name,)) dct[new_name] = func id_suffix[new_name] = getattr(func, '__x_extra_id__', '') class TestCase(unittest.TestCase, metaclass=TestGeneratorMetaclass): """Base class for test cases using the parameters decorator.""" def _OriginalName(self): return self._testMethodName.split(_SEPARATOR)[0] def __str__(self): return '%s (%s)' % (self._OriginalName(), _StrClass(self.__class__)) def id(self): # pylint: disable=invalid-name """Returns the descriptive ID of the test. This is used internally by the unittesting framework to get a name for the test to be used in reports. Returns: The test id. """ return '%s.%s%s' % (_StrClass(self.__class__), self._OriginalName(), self._id_suffix.get(self._testMethodName, '')) def CoopTestCase(other_base_class): """Returns a new base class with a cooperative metaclass base. This enables the TestCase to be used in combination with other base classes that have custom metaclasses, such as mox.MoxTestBase. Only works with metaclasses that do not override type.__new__. Example: import google3 import mox from google3.testing.pybase import parameterized class ExampleTest(parameterized.CoopTestCase(mox.MoxTestBase)): ... Args: other_base_class: (class) A test case base class. Returns: A new class object. """ metaclass = type( 'CoopMetaclass', (other_base_class.__metaclass__, TestGeneratorMetaclass), {}) return metaclass( 'CoopTestCase', (other_base_class, TestCase), {}) ================================================ FILE: openpype/hosts/nuke/vendor/google/protobuf/internal/api_implementation.py ================================================ # Protocol Buffers - Google's data interchange format # Copyright 2008 Google Inc. All rights reserved. # https://developers.google.com/protocol-buffers/ # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are # met: # # * Redistributions of source code must retain the above copyright # notice, this list of conditions and the following disclaimer. # * Redistributions in binary form must reproduce the above # copyright notice, this list of conditions and the following disclaimer # in the documentation and/or other materials provided with the # distribution. # * Neither the name of Google Inc. nor the names of its # contributors may be used to endorse or promote products derived from # this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. """Determine which implementation of the protobuf API is used in this process. """ import os import sys import warnings try: # pylint: disable=g-import-not-at-top from google.protobuf.internal import _api_implementation # The compile-time constants in the _api_implementation module can be used to # switch to a certain implementation of the Python API at build time. _api_version = _api_implementation.api_version except ImportError: _api_version = -1 # Unspecified by compiler flags. if _api_version == 1: raise ValueError('api_version=1 is no longer supported.') _default_implementation_type = ('cpp' if _api_version > 0 else 'python') # This environment variable can be used to switch to a certain implementation # of the Python API, overriding the compile-time constants in the # _api_implementation module. Right now only 'python' and 'cpp' are valid # values. Any other value will be ignored. _implementation_type = os.getenv('PROTOCOL_BUFFERS_PYTHON_IMPLEMENTATION', _default_implementation_type) if _implementation_type != 'python': _implementation_type = 'cpp' if 'PyPy' in sys.version and _implementation_type == 'cpp': warnings.warn('PyPy does not work yet with cpp protocol buffers. ' 'Falling back to the python implementation.') _implementation_type = 'python' # Detect if serialization should be deterministic by default try: # The presence of this module in a build allows the proto implementation to # be upgraded merely via build deps. # # NOTE: Merely importing this automatically enables deterministic proto # serialization for C++ code, but we still need to export it as a boolean so # that we can do the same for `_implementation_type == 'python'`. # # NOTE2: It is possible for C++ code to enable deterministic serialization by # default _without_ affecting Python code, if the C++ implementation is not in # use by this module. That is intended behavior, so we don't actually expose # this boolean outside of this module. # # pylint: disable=g-import-not-at-top,unused-import from google.protobuf import enable_deterministic_proto_serialization _python_deterministic_proto_serialization = True except ImportError: _python_deterministic_proto_serialization = False # Usage of this function is discouraged. Clients shouldn't care which # implementation of the API is in use. Note that there is no guarantee # that differences between APIs will be maintained. # Please don't use this function if possible. def Type(): return _implementation_type def _SetType(implementation_type): """Never use! Only for protobuf benchmark.""" global _implementation_type _implementation_type = implementation_type # See comment on 'Type' above. def Version(): return 2 # For internal use only def IsPythonDefaultSerializationDeterministic(): return _python_deterministic_proto_serialization ================================================ FILE: openpype/hosts/nuke/vendor/google/protobuf/internal/builder.py ================================================ # Protocol Buffers - Google's data interchange format # Copyright 2008 Google Inc. All rights reserved. # https://developers.google.com/protocol-buffers/ # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are # met: # # * Redistributions of source code must retain the above copyright # notice, this list of conditions and the following disclaimer. # * Redistributions in binary form must reproduce the above # copyright notice, this list of conditions and the following disclaimer # in the documentation and/or other materials provided with the # distribution. # * Neither the name of Google Inc. nor the names of its # contributors may be used to endorse or promote products derived from # this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. """Builds descriptors, message classes and services for generated _pb2.py. This file is only called in python generated _pb2.py files. It builds descriptors, message classes and services that users can directly use in generated code. """ __author__ = 'jieluo@google.com (Jie Luo)' from google.protobuf.internal import enum_type_wrapper from google.protobuf import message as _message from google.protobuf import reflection as _reflection from google.protobuf import symbol_database as _symbol_database _sym_db = _symbol_database.Default() def BuildMessageAndEnumDescriptors(file_des, module): """Builds message and enum descriptors. Args: file_des: FileDescriptor of the .proto file module: Generated _pb2 module """ def BuildNestedDescriptors(msg_des, prefix): for (name, nested_msg) in msg_des.nested_types_by_name.items(): module_name = prefix + name.upper() module[module_name] = nested_msg BuildNestedDescriptors(nested_msg, module_name + '_') for enum_des in msg_des.enum_types: module[prefix + enum_des.name.upper()] = enum_des for (name, msg_des) in file_des.message_types_by_name.items(): module_name = '_' + name.upper() module[module_name] = msg_des BuildNestedDescriptors(msg_des, module_name + '_') def BuildTopDescriptorsAndMessages(file_des, module_name, module): """Builds top level descriptors and message classes. Args: file_des: FileDescriptor of the .proto file module_name: str, the name of generated _pb2 module module: Generated _pb2 module """ def BuildMessage(msg_des): create_dict = {} for (name, nested_msg) in msg_des.nested_types_by_name.items(): create_dict[name] = BuildMessage(nested_msg) create_dict['DESCRIPTOR'] = msg_des create_dict['__module__'] = module_name message_class = _reflection.GeneratedProtocolMessageType( msg_des.name, (_message.Message,), create_dict) _sym_db.RegisterMessage(message_class) return message_class # top level enums for (name, enum_des) in file_des.enum_types_by_name.items(): module['_' + name.upper()] = enum_des module[name] = enum_type_wrapper.EnumTypeWrapper(enum_des) for enum_value in enum_des.values: module[enum_value.name] = enum_value.number # top level extensions for (name, extension_des) in file_des.extensions_by_name.items(): module[name.upper() + '_FIELD_NUMBER'] = extension_des.number module[name] = extension_des # services for (name, service) in file_des.services_by_name.items(): module['_' + name.upper()] = service # Build messages. for (name, msg_des) in file_des.message_types_by_name.items(): module[name] = BuildMessage(msg_des) def BuildServices(file_des, module_name, module): """Builds services classes and services stub class. Args: file_des: FileDescriptor of the .proto file module_name: str, the name of generated _pb2 module module: Generated _pb2 module """ # pylint: disable=g-import-not-at-top from google.protobuf import service as _service from google.protobuf import service_reflection # pylint: enable=g-import-not-at-top for (name, service) in file_des.services_by_name.items(): module[name] = service_reflection.GeneratedServiceType( name, (_service.Service,), dict(DESCRIPTOR=service, __module__=module_name)) stub_name = name + '_Stub' module[stub_name] = service_reflection.GeneratedServiceStubType( stub_name, (module[name],), dict(DESCRIPTOR=service, __module__=module_name)) ================================================ FILE: openpype/hosts/nuke/vendor/google/protobuf/internal/containers.py ================================================ # Protocol Buffers - Google's data interchange format # Copyright 2008 Google Inc. All rights reserved. # https://developers.google.com/protocol-buffers/ # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are # met: # # * Redistributions of source code must retain the above copyright # notice, this list of conditions and the following disclaimer. # * Redistributions in binary form must reproduce the above # copyright notice, this list of conditions and the following disclaimer # in the documentation and/or other materials provided with the # distribution. # * Neither the name of Google Inc. nor the names of its # contributors may be used to endorse or promote products derived from # this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. """Contains container classes to represent different protocol buffer types. This file defines container classes which represent categories of protocol buffer field types which need extra maintenance. Currently these categories are: - Repeated scalar fields - These are all repeated fields which aren't composite (e.g. they are of simple types like int32, string, etc). - Repeated composite fields - Repeated fields which are composite. This includes groups and nested messages. """ import collections.abc import copy import pickle from typing import ( Any, Iterable, Iterator, List, MutableMapping, MutableSequence, NoReturn, Optional, Sequence, TypeVar, Union, overload, ) _T = TypeVar('_T') _K = TypeVar('_K') _V = TypeVar('_V') class BaseContainer(Sequence[_T]): """Base container class.""" # Minimizes memory usage and disallows assignment to other attributes. __slots__ = ['_message_listener', '_values'] def __init__(self, message_listener: Any) -> None: """ Args: message_listener: A MessageListener implementation. The RepeatedScalarFieldContainer will call this object's Modified() method when it is modified. """ self._message_listener = message_listener self._values = [] @overload def __getitem__(self, key: int) -> _T: ... @overload def __getitem__(self, key: slice) -> List[_T]: ... def __getitem__(self, key): """Retrieves item by the specified key.""" return self._values[key] def __len__(self) -> int: """Returns the number of elements in the container.""" return len(self._values) def __ne__(self, other: Any) -> bool: """Checks if another instance isn't equal to this one.""" # The concrete classes should define __eq__. return not self == other __hash__ = None def __repr__(self) -> str: return repr(self._values) def sort(self, *args, **kwargs) -> None: # Continue to support the old sort_function keyword argument. # This is expected to be a rare occurrence, so use LBYL to avoid # the overhead of actually catching KeyError. if 'sort_function' in kwargs: kwargs['cmp'] = kwargs.pop('sort_function') self._values.sort(*args, **kwargs) def reverse(self) -> None: self._values.reverse() # TODO(slebedev): Remove this. BaseContainer does *not* conform to # MutableSequence, only its subclasses do. collections.abc.MutableSequence.register(BaseContainer) class RepeatedScalarFieldContainer(BaseContainer[_T], MutableSequence[_T]): """Simple, type-checked, list-like container for holding repeated scalars.""" # Disallows assignment to other attributes. __slots__ = ['_type_checker'] def __init__( self, message_listener: Any, type_checker: Any, ) -> None: """Args: message_listener: A MessageListener implementation. The RepeatedScalarFieldContainer will call this object's Modified() method when it is modified. type_checker: A type_checkers.ValueChecker instance to run on elements inserted into this container. """ super().__init__(message_listener) self._type_checker = type_checker def append(self, value: _T) -> None: """Appends an item to the list. Similar to list.append().""" self._values.append(self._type_checker.CheckValue(value)) if not self._message_listener.dirty: self._message_listener.Modified() def insert(self, key: int, value: _T) -> None: """Inserts the item at the specified position. Similar to list.insert().""" self._values.insert(key, self._type_checker.CheckValue(value)) if not self._message_listener.dirty: self._message_listener.Modified() def extend(self, elem_seq: Iterable[_T]) -> None: """Extends by appending the given iterable. Similar to list.extend().""" if elem_seq is None: return try: elem_seq_iter = iter(elem_seq) except TypeError: if not elem_seq: # silently ignore falsy inputs :-/. # TODO(ptucker): Deprecate this behavior. b/18413862 return raise new_values = [self._type_checker.CheckValue(elem) for elem in elem_seq_iter] if new_values: self._values.extend(new_values) self._message_listener.Modified() def MergeFrom( self, other: Union['RepeatedScalarFieldContainer[_T]', Iterable[_T]], ) -> None: """Appends the contents of another repeated field of the same type to this one. We do not check the types of the individual fields. """ self._values.extend(other) self._message_listener.Modified() def remove(self, elem: _T): """Removes an item from the list. Similar to list.remove().""" self._values.remove(elem) self._message_listener.Modified() def pop(self, key: Optional[int] = -1) -> _T: """Removes and returns an item at a given index. Similar to list.pop().""" value = self._values[key] self.__delitem__(key) return value @overload def __setitem__(self, key: int, value: _T) -> None: ... @overload def __setitem__(self, key: slice, value: Iterable[_T]) -> None: ... def __setitem__(self, key, value) -> None: """Sets the item on the specified position.""" if isinstance(key, slice): if key.step is not None: raise ValueError('Extended slices not supported') self._values[key] = map(self._type_checker.CheckValue, value) self._message_listener.Modified() else: self._values[key] = self._type_checker.CheckValue(value) self._message_listener.Modified() def __delitem__(self, key: Union[int, slice]) -> None: """Deletes the item at the specified position.""" del self._values[key] self._message_listener.Modified() def __eq__(self, other: Any) -> bool: """Compares the current instance with another one.""" if self is other: return True # Special case for the same type which should be common and fast. if isinstance(other, self.__class__): return other._values == self._values # We are presumably comparing against some other sequence type. return other == self._values def __deepcopy__( self, unused_memo: Any = None, ) -> 'RepeatedScalarFieldContainer[_T]': clone = RepeatedScalarFieldContainer( copy.deepcopy(self._message_listener), self._type_checker) clone.MergeFrom(self) return clone def __reduce__(self, **kwargs) -> NoReturn: raise pickle.PickleError( "Can't pickle repeated scalar fields, convert to list first") # TODO(slebedev): Constrain T to be a subtype of Message. class RepeatedCompositeFieldContainer(BaseContainer[_T], MutableSequence[_T]): """Simple, list-like container for holding repeated composite fields.""" # Disallows assignment to other attributes. __slots__ = ['_message_descriptor'] def __init__(self, message_listener: Any, message_descriptor: Any) -> None: """ Note that we pass in a descriptor instead of the generated directly, since at the time we construct a _RepeatedCompositeFieldContainer we haven't yet necessarily initialized the type that will be contained in the container. Args: message_listener: A MessageListener implementation. The RepeatedCompositeFieldContainer will call this object's Modified() method when it is modified. message_descriptor: A Descriptor instance describing the protocol type that should be present in this container. We'll use the _concrete_class field of this descriptor when the client calls add(). """ super().__init__(message_listener) self._message_descriptor = message_descriptor def add(self, **kwargs: Any) -> _T: """Adds a new element at the end of the list and returns it. Keyword arguments may be used to initialize the element. """ new_element = self._message_descriptor._concrete_class(**kwargs) new_element._SetListener(self._message_listener) self._values.append(new_element) if not self._message_listener.dirty: self._message_listener.Modified() return new_element def append(self, value: _T) -> None: """Appends one element by copying the message.""" new_element = self._message_descriptor._concrete_class() new_element._SetListener(self._message_listener) new_element.CopyFrom(value) self._values.append(new_element) if not self._message_listener.dirty: self._message_listener.Modified() def insert(self, key: int, value: _T) -> None: """Inserts the item at the specified position by copying.""" new_element = self._message_descriptor._concrete_class() new_element._SetListener(self._message_listener) new_element.CopyFrom(value) self._values.insert(key, new_element) if not self._message_listener.dirty: self._message_listener.Modified() def extend(self, elem_seq: Iterable[_T]) -> None: """Extends by appending the given sequence of elements of the same type as this one, copying each individual message. """ message_class = self._message_descriptor._concrete_class listener = self._message_listener values = self._values for message in elem_seq: new_element = message_class() new_element._SetListener(listener) new_element.MergeFrom(message) values.append(new_element) listener.Modified() def MergeFrom( self, other: Union['RepeatedCompositeFieldContainer[_T]', Iterable[_T]], ) -> None: """Appends the contents of another repeated field of the same type to this one, copying each individual message. """ self.extend(other) def remove(self, elem: _T) -> None: """Removes an item from the list. Similar to list.remove().""" self._values.remove(elem) self._message_listener.Modified() def pop(self, key: Optional[int] = -1) -> _T: """Removes and returns an item at a given index. Similar to list.pop().""" value = self._values[key] self.__delitem__(key) return value @overload def __setitem__(self, key: int, value: _T) -> None: ... @overload def __setitem__(self, key: slice, value: Iterable[_T]) -> None: ... def __setitem__(self, key, value): # This method is implemented to make RepeatedCompositeFieldContainer # structurally compatible with typing.MutableSequence. It is # otherwise unsupported and will always raise an error. raise TypeError( f'{self.__class__.__name__} object does not support item assignment') def __delitem__(self, key: Union[int, slice]) -> None: """Deletes the item at the specified position.""" del self._values[key] self._message_listener.Modified() def __eq__(self, other: Any) -> bool: """Compares the current instance with another one.""" if self is other: return True if not isinstance(other, self.__class__): raise TypeError('Can only compare repeated composite fields against ' 'other repeated composite fields.') return self._values == other._values class ScalarMap(MutableMapping[_K, _V]): """Simple, type-checked, dict-like container for holding repeated scalars.""" # Disallows assignment to other attributes. __slots__ = ['_key_checker', '_value_checker', '_values', '_message_listener', '_entry_descriptor'] def __init__( self, message_listener: Any, key_checker: Any, value_checker: Any, entry_descriptor: Any, ) -> None: """ Args: message_listener: A MessageListener implementation. The ScalarMap will call this object's Modified() method when it is modified. key_checker: A type_checkers.ValueChecker instance to run on keys inserted into this container. value_checker: A type_checkers.ValueChecker instance to run on values inserted into this container. entry_descriptor: The MessageDescriptor of a map entry: key and value. """ self._message_listener = message_listener self._key_checker = key_checker self._value_checker = value_checker self._entry_descriptor = entry_descriptor self._values = {} def __getitem__(self, key: _K) -> _V: try: return self._values[key] except KeyError: key = self._key_checker.CheckValue(key) val = self._value_checker.DefaultValue() self._values[key] = val return val def __contains__(self, item: _K) -> bool: # We check the key's type to match the strong-typing flavor of the API. # Also this makes it easier to match the behavior of the C++ implementation. self._key_checker.CheckValue(item) return item in self._values @overload def get(self, key: _K) -> Optional[_V]: ... @overload def get(self, key: _K, default: _T) -> Union[_V, _T]: ... # We need to override this explicitly, because our defaultdict-like behavior # will make the default implementation (from our base class) always insert # the key. def get(self, key, default=None): if key in self: return self[key] else: return default def __setitem__(self, key: _K, value: _V) -> _T: checked_key = self._key_checker.CheckValue(key) checked_value = self._value_checker.CheckValue(value) self._values[checked_key] = checked_value self._message_listener.Modified() def __delitem__(self, key: _K) -> None: del self._values[key] self._message_listener.Modified() def __len__(self) -> int: return len(self._values) def __iter__(self) -> Iterator[_K]: return iter(self._values) def __repr__(self) -> str: return repr(self._values) def MergeFrom(self, other: 'ScalarMap[_K, _V]') -> None: self._values.update(other._values) self._message_listener.Modified() def InvalidateIterators(self) -> None: # It appears that the only way to reliably invalidate iterators to # self._values is to ensure that its size changes. original = self._values self._values = original.copy() original[None] = None # This is defined in the abstract base, but we can do it much more cheaply. def clear(self) -> None: self._values.clear() self._message_listener.Modified() def GetEntryClass(self) -> Any: return self._entry_descriptor._concrete_class class MessageMap(MutableMapping[_K, _V]): """Simple, type-checked, dict-like container for with submessage values.""" # Disallows assignment to other attributes. __slots__ = ['_key_checker', '_values', '_message_listener', '_message_descriptor', '_entry_descriptor'] def __init__( self, message_listener: Any, message_descriptor: Any, key_checker: Any, entry_descriptor: Any, ) -> None: """ Args: message_listener: A MessageListener implementation. The ScalarMap will call this object's Modified() method when it is modified. key_checker: A type_checkers.ValueChecker instance to run on keys inserted into this container. value_checker: A type_checkers.ValueChecker instance to run on values inserted into this container. entry_descriptor: The MessageDescriptor of a map entry: key and value. """ self._message_listener = message_listener self._message_descriptor = message_descriptor self._key_checker = key_checker self._entry_descriptor = entry_descriptor self._values = {} def __getitem__(self, key: _K) -> _V: key = self._key_checker.CheckValue(key) try: return self._values[key] except KeyError: new_element = self._message_descriptor._concrete_class() new_element._SetListener(self._message_listener) self._values[key] = new_element self._message_listener.Modified() return new_element def get_or_create(self, key: _K) -> _V: """get_or_create() is an alias for getitem (ie. map[key]). Args: key: The key to get or create in the map. This is useful in cases where you want to be explicit that the call is mutating the map. This can avoid lint errors for statements like this that otherwise would appear to be pointless statements: msg.my_map[key] """ return self[key] @overload def get(self, key: _K) -> Optional[_V]: ... @overload def get(self, key: _K, default: _T) -> Union[_V, _T]: ... # We need to override this explicitly, because our defaultdict-like behavior # will make the default implementation (from our base class) always insert # the key. def get(self, key, default=None): if key in self: return self[key] else: return default def __contains__(self, item: _K) -> bool: item = self._key_checker.CheckValue(item) return item in self._values def __setitem__(self, key: _K, value: _V) -> NoReturn: raise ValueError('May not set values directly, call my_map[key].foo = 5') def __delitem__(self, key: _K) -> None: key = self._key_checker.CheckValue(key) del self._values[key] self._message_listener.Modified() def __len__(self) -> int: return len(self._values) def __iter__(self) -> Iterator[_K]: return iter(self._values) def __repr__(self) -> str: return repr(self._values) def MergeFrom(self, other: 'MessageMap[_K, _V]') -> None: # pylint: disable=protected-access for key in other._values: # According to documentation: "When parsing from the wire or when merging, # if there are duplicate map keys the last key seen is used". if key in self: del self[key] self[key].CopyFrom(other[key]) # self._message_listener.Modified() not required here, because # mutations to submessages already propagate. def InvalidateIterators(self) -> None: # It appears that the only way to reliably invalidate iterators to # self._values is to ensure that its size changes. original = self._values self._values = original.copy() original[None] = None # This is defined in the abstract base, but we can do it much more cheaply. def clear(self) -> None: self._values.clear() self._message_listener.Modified() def GetEntryClass(self) -> Any: return self._entry_descriptor._concrete_class class _UnknownField: """A parsed unknown field.""" # Disallows assignment to other attributes. __slots__ = ['_field_number', '_wire_type', '_data'] def __init__(self, field_number, wire_type, data): self._field_number = field_number self._wire_type = wire_type self._data = data return def __lt__(self, other): # pylint: disable=protected-access return self._field_number < other._field_number def __eq__(self, other): if self is other: return True # pylint: disable=protected-access return (self._field_number == other._field_number and self._wire_type == other._wire_type and self._data == other._data) class UnknownFieldRef: # pylint: disable=missing-class-docstring def __init__(self, parent, index): self._parent = parent self._index = index def _check_valid(self): if not self._parent: raise ValueError('UnknownField does not exist. ' 'The parent message might be cleared.') if self._index >= len(self._parent): raise ValueError('UnknownField does not exist. ' 'The parent message might be cleared.') @property def field_number(self): self._check_valid() # pylint: disable=protected-access return self._parent._internal_get(self._index)._field_number @property def wire_type(self): self._check_valid() # pylint: disable=protected-access return self._parent._internal_get(self._index)._wire_type @property def data(self): self._check_valid() # pylint: disable=protected-access return self._parent._internal_get(self._index)._data class UnknownFieldSet: """UnknownField container""" # Disallows assignment to other attributes. __slots__ = ['_values'] def __init__(self): self._values = [] def __getitem__(self, index): if self._values is None: raise ValueError('UnknownFields does not exist. ' 'The parent message might be cleared.') size = len(self._values) if index < 0: index += size if index < 0 or index >= size: raise IndexError('index %d out of range'.index) return UnknownFieldRef(self, index) def _internal_get(self, index): return self._values[index] def __len__(self): if self._values is None: raise ValueError('UnknownFields does not exist. ' 'The parent message might be cleared.') return len(self._values) def _add(self, field_number, wire_type, data): unknown_field = _UnknownField(field_number, wire_type, data) self._values.append(unknown_field) return unknown_field def __iter__(self): for i in range(len(self)): yield UnknownFieldRef(self, i) def _extend(self, other): if other is None: return # pylint: disable=protected-access self._values.extend(other._values) def __eq__(self, other): if self is other: return True # Sort unknown fields because their order shouldn't # affect equality test. values = list(self._values) if other is None: return not values values.sort() # pylint: disable=protected-access other_values = sorted(other._values) return values == other_values def _clear(self): for value in self._values: # pylint: disable=protected-access if isinstance(value._data, UnknownFieldSet): value._data._clear() # pylint: disable=protected-access self._values = None ================================================ FILE: openpype/hosts/nuke/vendor/google/protobuf/internal/decoder.py ================================================ # Protocol Buffers - Google's data interchange format # Copyright 2008 Google Inc. All rights reserved. # https://developers.google.com/protocol-buffers/ # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are # met: # # * Redistributions of source code must retain the above copyright # notice, this list of conditions and the following disclaimer. # * Redistributions in binary form must reproduce the above # copyright notice, this list of conditions and the following disclaimer # in the documentation and/or other materials provided with the # distribution. # * Neither the name of Google Inc. nor the names of its # contributors may be used to endorse or promote products derived from # this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. """Code for decoding protocol buffer primitives. This code is very similar to encoder.py -- read the docs for that module first. A "decoder" is a function with the signature: Decode(buffer, pos, end, message, field_dict) The arguments are: buffer: The string containing the encoded message. pos: The current position in the string. end: The position in the string where the current message ends. May be less than len(buffer) if we're reading a sub-message. message: The message object into which we're parsing. field_dict: message._fields (avoids a hashtable lookup). The decoder reads the field and stores it into field_dict, returning the new buffer position. A decoder for a repeated field may proactively decode all of the elements of that field, if they appear consecutively. Note that decoders may throw any of the following: IndexError: Indicates a truncated message. struct.error: Unpacking of a fixed-width field failed. message.DecodeError: Other errors. Decoders are expected to raise an exception if they are called with pos > end. This allows callers to be lax about bounds checking: it's fineto read past "end" as long as you are sure that someone else will notice and throw an exception later on. Something up the call stack is expected to catch IndexError and struct.error and convert them to message.DecodeError. Decoders are constructed using decoder constructors with the signature: MakeDecoder(field_number, is_repeated, is_packed, key, new_default) The arguments are: field_number: The field number of the field we want to decode. is_repeated: Is the field a repeated field? (bool) is_packed: Is the field a packed field? (bool) key: The key to use when looking up the field within field_dict. (This is actually the FieldDescriptor but nothing in this file should depend on that.) new_default: A function which takes a message object as a parameter and returns a new instance of the default value for this field. (This is called for repeated fields and sub-messages, when an instance does not already exist.) As with encoders, we define a decoder constructor for every type of field. Then, for every field of every message class we construct an actual decoder. That decoder goes into a dict indexed by tag, so when we decode a message we repeatedly read a tag, look up the corresponding decoder, and invoke it. """ __author__ = 'kenton@google.com (Kenton Varda)' import math import struct from google.protobuf.internal import containers from google.protobuf.internal import encoder from google.protobuf.internal import wire_format from google.protobuf import message # This is not for optimization, but rather to avoid conflicts with local # variables named "message". _DecodeError = message.DecodeError def _VarintDecoder(mask, result_type): """Return an encoder for a basic varint value (does not include tag). Decoded values will be bitwise-anded with the given mask before being returned, e.g. to limit them to 32 bits. The returned decoder does not take the usual "end" parameter -- the caller is expected to do bounds checking after the fact (often the caller can defer such checking until later). The decoder returns a (value, new_pos) pair. """ def DecodeVarint(buffer, pos): result = 0 shift = 0 while 1: b = buffer[pos] result |= ((b & 0x7f) << shift) pos += 1 if not (b & 0x80): result &= mask result = result_type(result) return (result, pos) shift += 7 if shift >= 64: raise _DecodeError('Too many bytes when decoding varint.') return DecodeVarint def _SignedVarintDecoder(bits, result_type): """Like _VarintDecoder() but decodes signed values.""" signbit = 1 << (bits - 1) mask = (1 << bits) - 1 def DecodeVarint(buffer, pos): result = 0 shift = 0 while 1: b = buffer[pos] result |= ((b & 0x7f) << shift) pos += 1 if not (b & 0x80): result &= mask result = (result ^ signbit) - signbit result = result_type(result) return (result, pos) shift += 7 if shift >= 64: raise _DecodeError('Too many bytes when decoding varint.') return DecodeVarint # All 32-bit and 64-bit values are represented as int. _DecodeVarint = _VarintDecoder((1 << 64) - 1, int) _DecodeSignedVarint = _SignedVarintDecoder(64, int) # Use these versions for values which must be limited to 32 bits. _DecodeVarint32 = _VarintDecoder((1 << 32) - 1, int) _DecodeSignedVarint32 = _SignedVarintDecoder(32, int) def ReadTag(buffer, pos): """Read a tag from the memoryview, and return a (tag_bytes, new_pos) tuple. We return the raw bytes of the tag rather than decoding them. The raw bytes can then be used to look up the proper decoder. This effectively allows us to trade some work that would be done in pure-python (decoding a varint) for work that is done in C (searching for a byte string in a hash table). In a low-level language it would be much cheaper to decode the varint and use that, but not in Python. Args: buffer: memoryview object of the encoded bytes pos: int of the current position to start from Returns: Tuple[bytes, int] of the tag data and new position. """ start = pos while buffer[pos] & 0x80: pos += 1 pos += 1 tag_bytes = buffer[start:pos].tobytes() return tag_bytes, pos # -------------------------------------------------------------------- def _SimpleDecoder(wire_type, decode_value): """Return a constructor for a decoder for fields of a particular type. Args: wire_type: The field's wire type. decode_value: A function which decodes an individual value, e.g. _DecodeVarint() """ def SpecificDecoder(field_number, is_repeated, is_packed, key, new_default, clear_if_default=False): if is_packed: local_DecodeVarint = _DecodeVarint def DecodePackedField(buffer, pos, end, message, field_dict): value = field_dict.get(key) if value is None: value = field_dict.setdefault(key, new_default(message)) (endpoint, pos) = local_DecodeVarint(buffer, pos) endpoint += pos if endpoint > end: raise _DecodeError('Truncated message.') while pos < endpoint: (element, pos) = decode_value(buffer, pos) value.append(element) if pos > endpoint: del value[-1] # Discard corrupt value. raise _DecodeError('Packed element was truncated.') return pos return DecodePackedField elif is_repeated: tag_bytes = encoder.TagBytes(field_number, wire_type) tag_len = len(tag_bytes) def DecodeRepeatedField(buffer, pos, end, message, field_dict): value = field_dict.get(key) if value is None: value = field_dict.setdefault(key, new_default(message)) while 1: (element, new_pos) = decode_value(buffer, pos) value.append(element) # Predict that the next tag is another copy of the same repeated # field. pos = new_pos + tag_len if buffer[new_pos:pos] != tag_bytes or new_pos >= end: # Prediction failed. Return. if new_pos > end: raise _DecodeError('Truncated message.') return new_pos return DecodeRepeatedField else: def DecodeField(buffer, pos, end, message, field_dict): (new_value, pos) = decode_value(buffer, pos) if pos > end: raise _DecodeError('Truncated message.') if clear_if_default and not new_value: field_dict.pop(key, None) else: field_dict[key] = new_value return pos return DecodeField return SpecificDecoder def _ModifiedDecoder(wire_type, decode_value, modify_value): """Like SimpleDecoder but additionally invokes modify_value on every value before storing it. Usually modify_value is ZigZagDecode. """ # Reusing _SimpleDecoder is slightly slower than copying a bunch of code, but # not enough to make a significant difference. def InnerDecode(buffer, pos): (result, new_pos) = decode_value(buffer, pos) return (modify_value(result), new_pos) return _SimpleDecoder(wire_type, InnerDecode) def _StructPackDecoder(wire_type, format): """Return a constructor for a decoder for a fixed-width field. Args: wire_type: The field's wire type. format: The format string to pass to struct.unpack(). """ value_size = struct.calcsize(format) local_unpack = struct.unpack # Reusing _SimpleDecoder is slightly slower than copying a bunch of code, but # not enough to make a significant difference. # Note that we expect someone up-stack to catch struct.error and convert # it to _DecodeError -- this way we don't have to set up exception- # handling blocks every time we parse one value. def InnerDecode(buffer, pos): new_pos = pos + value_size result = local_unpack(format, buffer[pos:new_pos])[0] return (result, new_pos) return _SimpleDecoder(wire_type, InnerDecode) def _FloatDecoder(): """Returns a decoder for a float field. This code works around a bug in struct.unpack for non-finite 32-bit floating-point values. """ local_unpack = struct.unpack def InnerDecode(buffer, pos): """Decode serialized float to a float and new position. Args: buffer: memoryview of the serialized bytes pos: int, position in the memory view to start at. Returns: Tuple[float, int] of the deserialized float value and new position in the serialized data. """ # We expect a 32-bit value in little-endian byte order. Bit 1 is the sign # bit, bits 2-9 represent the exponent, and bits 10-32 are the significand. new_pos = pos + 4 float_bytes = buffer[pos:new_pos].tobytes() # If this value has all its exponent bits set, then it's non-finite. # In Python 2.4, struct.unpack will convert it to a finite 64-bit value. # To avoid that, we parse it specially. if (float_bytes[3:4] in b'\x7F\xFF' and float_bytes[2:3] >= b'\x80'): # If at least one significand bit is set... if float_bytes[0:3] != b'\x00\x00\x80': return (math.nan, new_pos) # If sign bit is set... if float_bytes[3:4] == b'\xFF': return (-math.inf, new_pos) return (math.inf, new_pos) # Note that we expect someone up-stack to catch struct.error and convert # it to _DecodeError -- this way we don't have to set up exception- # handling blocks every time we parse one value. result = local_unpack('= b'\xF0') and (double_bytes[0:7] != b'\x00\x00\x00\x00\x00\x00\xF0')): return (math.nan, new_pos) # Note that we expect someone up-stack to catch struct.error and convert # it to _DecodeError -- this way we don't have to set up exception- # handling blocks every time we parse one value. result = local_unpack(' end: raise _DecodeError('Truncated message.') while pos < endpoint: value_start_pos = pos (element, pos) = _DecodeSignedVarint32(buffer, pos) # pylint: disable=protected-access if element in enum_type.values_by_number: value.append(element) else: if not message._unknown_fields: message._unknown_fields = [] tag_bytes = encoder.TagBytes(field_number, wire_format.WIRETYPE_VARINT) message._unknown_fields.append( (tag_bytes, buffer[value_start_pos:pos].tobytes())) if message._unknown_field_set is None: message._unknown_field_set = containers.UnknownFieldSet() message._unknown_field_set._add( field_number, wire_format.WIRETYPE_VARINT, element) # pylint: enable=protected-access if pos > endpoint: if element in enum_type.values_by_number: del value[-1] # Discard corrupt value. else: del message._unknown_fields[-1] # pylint: disable=protected-access del message._unknown_field_set._values[-1] # pylint: enable=protected-access raise _DecodeError('Packed element was truncated.') return pos return DecodePackedField elif is_repeated: tag_bytes = encoder.TagBytes(field_number, wire_format.WIRETYPE_VARINT) tag_len = len(tag_bytes) def DecodeRepeatedField(buffer, pos, end, message, field_dict): """Decode serialized repeated enum to its value and a new position. Args: buffer: memoryview of the serialized bytes. pos: int, position in the memory view to start at. end: int, end position of serialized data message: Message object to store unknown fields in field_dict: Map[Descriptor, Any] to store decoded values in. Returns: int, new position in serialized data. """ value = field_dict.get(key) if value is None: value = field_dict.setdefault(key, new_default(message)) while 1: (element, new_pos) = _DecodeSignedVarint32(buffer, pos) # pylint: disable=protected-access if element in enum_type.values_by_number: value.append(element) else: if not message._unknown_fields: message._unknown_fields = [] message._unknown_fields.append( (tag_bytes, buffer[pos:new_pos].tobytes())) if message._unknown_field_set is None: message._unknown_field_set = containers.UnknownFieldSet() message._unknown_field_set._add( field_number, wire_format.WIRETYPE_VARINT, element) # pylint: enable=protected-access # Predict that the next tag is another copy of the same repeated # field. pos = new_pos + tag_len if buffer[new_pos:pos] != tag_bytes or new_pos >= end: # Prediction failed. Return. if new_pos > end: raise _DecodeError('Truncated message.') return new_pos return DecodeRepeatedField else: def DecodeField(buffer, pos, end, message, field_dict): """Decode serialized repeated enum to its value and a new position. Args: buffer: memoryview of the serialized bytes. pos: int, position in the memory view to start at. end: int, end position of serialized data message: Message object to store unknown fields in field_dict: Map[Descriptor, Any] to store decoded values in. Returns: int, new position in serialized data. """ value_start_pos = pos (enum_value, pos) = _DecodeSignedVarint32(buffer, pos) if pos > end: raise _DecodeError('Truncated message.') if clear_if_default and not enum_value: field_dict.pop(key, None) return pos # pylint: disable=protected-access if enum_value in enum_type.values_by_number: field_dict[key] = enum_value else: if not message._unknown_fields: message._unknown_fields = [] tag_bytes = encoder.TagBytes(field_number, wire_format.WIRETYPE_VARINT) message._unknown_fields.append( (tag_bytes, buffer[value_start_pos:pos].tobytes())) if message._unknown_field_set is None: message._unknown_field_set = containers.UnknownFieldSet() message._unknown_field_set._add( field_number, wire_format.WIRETYPE_VARINT, enum_value) # pylint: enable=protected-access return pos return DecodeField # -------------------------------------------------------------------- Int32Decoder = _SimpleDecoder( wire_format.WIRETYPE_VARINT, _DecodeSignedVarint32) Int64Decoder = _SimpleDecoder( wire_format.WIRETYPE_VARINT, _DecodeSignedVarint) UInt32Decoder = _SimpleDecoder(wire_format.WIRETYPE_VARINT, _DecodeVarint32) UInt64Decoder = _SimpleDecoder(wire_format.WIRETYPE_VARINT, _DecodeVarint) SInt32Decoder = _ModifiedDecoder( wire_format.WIRETYPE_VARINT, _DecodeVarint32, wire_format.ZigZagDecode) SInt64Decoder = _ModifiedDecoder( wire_format.WIRETYPE_VARINT, _DecodeVarint, wire_format.ZigZagDecode) # Note that Python conveniently guarantees that when using the '<' prefix on # formats, they will also have the same size across all platforms (as opposed # to without the prefix, where their sizes depend on the C compiler's basic # type sizes). Fixed32Decoder = _StructPackDecoder(wire_format.WIRETYPE_FIXED32, ' end: raise _DecodeError('Truncated string.') value.append(_ConvertToUnicode(buffer[pos:new_pos])) # Predict that the next tag is another copy of the same repeated field. pos = new_pos + tag_len if buffer[new_pos:pos] != tag_bytes or new_pos == end: # Prediction failed. Return. return new_pos return DecodeRepeatedField else: def DecodeField(buffer, pos, end, message, field_dict): (size, pos) = local_DecodeVarint(buffer, pos) new_pos = pos + size if new_pos > end: raise _DecodeError('Truncated string.') if clear_if_default and not size: field_dict.pop(key, None) else: field_dict[key] = _ConvertToUnicode(buffer[pos:new_pos]) return new_pos return DecodeField def BytesDecoder(field_number, is_repeated, is_packed, key, new_default, clear_if_default=False): """Returns a decoder for a bytes field.""" local_DecodeVarint = _DecodeVarint assert not is_packed if is_repeated: tag_bytes = encoder.TagBytes(field_number, wire_format.WIRETYPE_LENGTH_DELIMITED) tag_len = len(tag_bytes) def DecodeRepeatedField(buffer, pos, end, message, field_dict): value = field_dict.get(key) if value is None: value = field_dict.setdefault(key, new_default(message)) while 1: (size, pos) = local_DecodeVarint(buffer, pos) new_pos = pos + size if new_pos > end: raise _DecodeError('Truncated string.') value.append(buffer[pos:new_pos].tobytes()) # Predict that the next tag is another copy of the same repeated field. pos = new_pos + tag_len if buffer[new_pos:pos] != tag_bytes or new_pos == end: # Prediction failed. Return. return new_pos return DecodeRepeatedField else: def DecodeField(buffer, pos, end, message, field_dict): (size, pos) = local_DecodeVarint(buffer, pos) new_pos = pos + size if new_pos > end: raise _DecodeError('Truncated string.') if clear_if_default and not size: field_dict.pop(key, None) else: field_dict[key] = buffer[pos:new_pos].tobytes() return new_pos return DecodeField def GroupDecoder(field_number, is_repeated, is_packed, key, new_default): """Returns a decoder for a group field.""" end_tag_bytes = encoder.TagBytes(field_number, wire_format.WIRETYPE_END_GROUP) end_tag_len = len(end_tag_bytes) assert not is_packed if is_repeated: tag_bytes = encoder.TagBytes(field_number, wire_format.WIRETYPE_START_GROUP) tag_len = len(tag_bytes) def DecodeRepeatedField(buffer, pos, end, message, field_dict): value = field_dict.get(key) if value is None: value = field_dict.setdefault(key, new_default(message)) while 1: value = field_dict.get(key) if value is None: value = field_dict.setdefault(key, new_default(message)) # Read sub-message. pos = value.add()._InternalParse(buffer, pos, end) # Read end tag. new_pos = pos+end_tag_len if buffer[pos:new_pos] != end_tag_bytes or new_pos > end: raise _DecodeError('Missing group end tag.') # Predict that the next tag is another copy of the same repeated field. pos = new_pos + tag_len if buffer[new_pos:pos] != tag_bytes or new_pos == end: # Prediction failed. Return. return new_pos return DecodeRepeatedField else: def DecodeField(buffer, pos, end, message, field_dict): value = field_dict.get(key) if value is None: value = field_dict.setdefault(key, new_default(message)) # Read sub-message. pos = value._InternalParse(buffer, pos, end) # Read end tag. new_pos = pos+end_tag_len if buffer[pos:new_pos] != end_tag_bytes or new_pos > end: raise _DecodeError('Missing group end tag.') return new_pos return DecodeField def MessageDecoder(field_number, is_repeated, is_packed, key, new_default): """Returns a decoder for a message field.""" local_DecodeVarint = _DecodeVarint assert not is_packed if is_repeated: tag_bytes = encoder.TagBytes(field_number, wire_format.WIRETYPE_LENGTH_DELIMITED) tag_len = len(tag_bytes) def DecodeRepeatedField(buffer, pos, end, message, field_dict): value = field_dict.get(key) if value is None: value = field_dict.setdefault(key, new_default(message)) while 1: # Read length. (size, pos) = local_DecodeVarint(buffer, pos) new_pos = pos + size if new_pos > end: raise _DecodeError('Truncated message.') # Read sub-message. if value.add()._InternalParse(buffer, pos, new_pos) != new_pos: # The only reason _InternalParse would return early is if it # encountered an end-group tag. raise _DecodeError('Unexpected end-group tag.') # Predict that the next tag is another copy of the same repeated field. pos = new_pos + tag_len if buffer[new_pos:pos] != tag_bytes or new_pos == end: # Prediction failed. Return. return new_pos return DecodeRepeatedField else: def DecodeField(buffer, pos, end, message, field_dict): value = field_dict.get(key) if value is None: value = field_dict.setdefault(key, new_default(message)) # Read length. (size, pos) = local_DecodeVarint(buffer, pos) new_pos = pos + size if new_pos > end: raise _DecodeError('Truncated message.') # Read sub-message. if value._InternalParse(buffer, pos, new_pos) != new_pos: # The only reason _InternalParse would return early is if it encountered # an end-group tag. raise _DecodeError('Unexpected end-group tag.') return new_pos return DecodeField # -------------------------------------------------------------------- MESSAGE_SET_ITEM_TAG = encoder.TagBytes(1, wire_format.WIRETYPE_START_GROUP) def MessageSetItemDecoder(descriptor): """Returns a decoder for a MessageSet item. The parameter is the message Descriptor. The message set message looks like this: message MessageSet { repeated group Item = 1 { required int32 type_id = 2; required string message = 3; } } """ type_id_tag_bytes = encoder.TagBytes(2, wire_format.WIRETYPE_VARINT) message_tag_bytes = encoder.TagBytes(3, wire_format.WIRETYPE_LENGTH_DELIMITED) item_end_tag_bytes = encoder.TagBytes(1, wire_format.WIRETYPE_END_GROUP) local_ReadTag = ReadTag local_DecodeVarint = _DecodeVarint local_SkipField = SkipField def DecodeItem(buffer, pos, end, message, field_dict): """Decode serialized message set to its value and new position. Args: buffer: memoryview of the serialized bytes. pos: int, position in the memory view to start at. end: int, end position of serialized data message: Message object to store unknown fields in field_dict: Map[Descriptor, Any] to store decoded values in. Returns: int, new position in serialized data. """ message_set_item_start = pos type_id = -1 message_start = -1 message_end = -1 # Technically, type_id and message can appear in any order, so we need # a little loop here. while 1: (tag_bytes, pos) = local_ReadTag(buffer, pos) if tag_bytes == type_id_tag_bytes: (type_id, pos) = local_DecodeVarint(buffer, pos) elif tag_bytes == message_tag_bytes: (size, message_start) = local_DecodeVarint(buffer, pos) pos = message_end = message_start + size elif tag_bytes == item_end_tag_bytes: break else: pos = SkipField(buffer, pos, end, tag_bytes) if pos == -1: raise _DecodeError('Missing group end tag.') if pos > end: raise _DecodeError('Truncated message.') if type_id == -1: raise _DecodeError('MessageSet item missing type_id.') if message_start == -1: raise _DecodeError('MessageSet item missing message.') extension = message.Extensions._FindExtensionByNumber(type_id) # pylint: disable=protected-access if extension is not None: value = field_dict.get(extension) if value is None: message_type = extension.message_type if not hasattr(message_type, '_concrete_class'): # pylint: disable=protected-access message._FACTORY.GetPrototype(message_type) value = field_dict.setdefault( extension, message_type._concrete_class()) if value._InternalParse(buffer, message_start,message_end) != message_end: # The only reason _InternalParse would return early is if it encountered # an end-group tag. raise _DecodeError('Unexpected end-group tag.') else: if not message._unknown_fields: message._unknown_fields = [] message._unknown_fields.append( (MESSAGE_SET_ITEM_TAG, buffer[message_set_item_start:pos].tobytes())) if message._unknown_field_set is None: message._unknown_field_set = containers.UnknownFieldSet() message._unknown_field_set._add( type_id, wire_format.WIRETYPE_LENGTH_DELIMITED, buffer[message_start:message_end].tobytes()) # pylint: enable=protected-access return pos return DecodeItem # -------------------------------------------------------------------- def MapDecoder(field_descriptor, new_default, is_message_map): """Returns a decoder for a map field.""" key = field_descriptor tag_bytes = encoder.TagBytes(field_descriptor.number, wire_format.WIRETYPE_LENGTH_DELIMITED) tag_len = len(tag_bytes) local_DecodeVarint = _DecodeVarint # Can't read _concrete_class yet; might not be initialized. message_type = field_descriptor.message_type def DecodeMap(buffer, pos, end, message, field_dict): submsg = message_type._concrete_class() value = field_dict.get(key) if value is None: value = field_dict.setdefault(key, new_default(message)) while 1: # Read length. (size, pos) = local_DecodeVarint(buffer, pos) new_pos = pos + size if new_pos > end: raise _DecodeError('Truncated message.') # Read sub-message. submsg.Clear() if submsg._InternalParse(buffer, pos, new_pos) != new_pos: # The only reason _InternalParse would return early is if it # encountered an end-group tag. raise _DecodeError('Unexpected end-group tag.') if is_message_map: value[submsg.key].CopyFrom(submsg.value) else: value[submsg.key] = submsg.value # Predict that the next tag is another copy of the same repeated field. pos = new_pos + tag_len if buffer[new_pos:pos] != tag_bytes or new_pos == end: # Prediction failed. Return. return new_pos return DecodeMap # -------------------------------------------------------------------- # Optimization is not as heavy here because calls to SkipField() are rare, # except for handling end-group tags. def _SkipVarint(buffer, pos, end): """Skip a varint value. Returns the new position.""" # Previously ord(buffer[pos]) raised IndexError when pos is out of range. # With this code, ord(b'') raises TypeError. Both are handled in # python_message.py to generate a 'Truncated message' error. while ord(buffer[pos:pos+1].tobytes()) & 0x80: pos += 1 pos += 1 if pos > end: raise _DecodeError('Truncated message.') return pos def _SkipFixed64(buffer, pos, end): """Skip a fixed64 value. Returns the new position.""" pos += 8 if pos > end: raise _DecodeError('Truncated message.') return pos def _DecodeFixed64(buffer, pos): """Decode a fixed64.""" new_pos = pos + 8 return (struct.unpack(' end: raise _DecodeError('Truncated message.') return pos def _SkipGroup(buffer, pos, end): """Skip sub-group. Returns the new position.""" while 1: (tag_bytes, pos) = ReadTag(buffer, pos) new_pos = SkipField(buffer, pos, end, tag_bytes) if new_pos == -1: return pos pos = new_pos def _DecodeUnknownFieldSet(buffer, pos, end_pos=None): """Decode UnknownFieldSet. Returns the UnknownFieldSet and new position.""" unknown_field_set = containers.UnknownFieldSet() while end_pos is None or pos < end_pos: (tag_bytes, pos) = ReadTag(buffer, pos) (tag, _) = _DecodeVarint(tag_bytes, 0) field_number, wire_type = wire_format.UnpackTag(tag) if wire_type == wire_format.WIRETYPE_END_GROUP: break (data, pos) = _DecodeUnknownField(buffer, pos, wire_type) # pylint: disable=protected-access unknown_field_set._add(field_number, wire_type, data) return (unknown_field_set, pos) def _DecodeUnknownField(buffer, pos, wire_type): """Decode a unknown field. Returns the UnknownField and new position.""" if wire_type == wire_format.WIRETYPE_VARINT: (data, pos) = _DecodeVarint(buffer, pos) elif wire_type == wire_format.WIRETYPE_FIXED64: (data, pos) = _DecodeFixed64(buffer, pos) elif wire_type == wire_format.WIRETYPE_FIXED32: (data, pos) = _DecodeFixed32(buffer, pos) elif wire_type == wire_format.WIRETYPE_LENGTH_DELIMITED: (size, pos) = _DecodeVarint(buffer, pos) data = buffer[pos:pos+size].tobytes() pos += size elif wire_type == wire_format.WIRETYPE_START_GROUP: (data, pos) = _DecodeUnknownFieldSet(buffer, pos) elif wire_type == wire_format.WIRETYPE_END_GROUP: return (0, -1) else: raise _DecodeError('Wrong wire type in tag.') return (data, pos) def _EndGroup(buffer, pos, end): """Skipping an END_GROUP tag returns -1 to tell the parent loop to break.""" return -1 def _SkipFixed32(buffer, pos, end): """Skip a fixed32 value. Returns the new position.""" pos += 4 if pos > end: raise _DecodeError('Truncated message.') return pos def _DecodeFixed32(buffer, pos): """Decode a fixed32.""" new_pos = pos + 4 return (struct.unpack('B').pack def EncodeVarint(write, value, unused_deterministic=None): bits = value & 0x7f value >>= 7 while value: write(local_int2byte(0x80|bits)) bits = value & 0x7f value >>= 7 return write(local_int2byte(bits)) return EncodeVarint def _SignedVarintEncoder(): """Return an encoder for a basic signed varint value (does not include tag).""" local_int2byte = struct.Struct('>B').pack def EncodeSignedVarint(write, value, unused_deterministic=None): if value < 0: value += (1 << 64) bits = value & 0x7f value >>= 7 while value: write(local_int2byte(0x80|bits)) bits = value & 0x7f value >>= 7 return write(local_int2byte(bits)) return EncodeSignedVarint _EncodeVarint = _VarintEncoder() _EncodeSignedVarint = _SignedVarintEncoder() def _VarintBytes(value): """Encode the given integer as a varint and return the bytes. This is only called at startup time so it doesn't need to be fast.""" pieces = [] _EncodeVarint(pieces.append, value, True) return b"".join(pieces) def TagBytes(field_number, wire_type): """Encode the given tag and return the bytes. Only called at startup.""" return bytes(_VarintBytes(wire_format.PackTag(field_number, wire_type))) # -------------------------------------------------------------------- # As with sizers (see above), we have a number of common encoder # implementations. def _SimpleEncoder(wire_type, encode_value, compute_value_size): """Return a constructor for an encoder for fields of a particular type. Args: wire_type: The field's wire type, for encoding tags. encode_value: A function which encodes an individual value, e.g. _EncodeVarint(). compute_value_size: A function which computes the size of an individual value, e.g. _VarintSize(). """ def SpecificEncoder(field_number, is_repeated, is_packed): if is_packed: tag_bytes = TagBytes(field_number, wire_format.WIRETYPE_LENGTH_DELIMITED) local_EncodeVarint = _EncodeVarint def EncodePackedField(write, value, deterministic): write(tag_bytes) size = 0 for element in value: size += compute_value_size(element) local_EncodeVarint(write, size, deterministic) for element in value: encode_value(write, element, deterministic) return EncodePackedField elif is_repeated: tag_bytes = TagBytes(field_number, wire_type) def EncodeRepeatedField(write, value, deterministic): for element in value: write(tag_bytes) encode_value(write, element, deterministic) return EncodeRepeatedField else: tag_bytes = TagBytes(field_number, wire_type) def EncodeField(write, value, deterministic): write(tag_bytes) return encode_value(write, value, deterministic) return EncodeField return SpecificEncoder def _ModifiedEncoder(wire_type, encode_value, compute_value_size, modify_value): """Like SimpleEncoder but additionally invokes modify_value on every value before passing it to encode_value. Usually modify_value is ZigZagEncode.""" def SpecificEncoder(field_number, is_repeated, is_packed): if is_packed: tag_bytes = TagBytes(field_number, wire_format.WIRETYPE_LENGTH_DELIMITED) local_EncodeVarint = _EncodeVarint def EncodePackedField(write, value, deterministic): write(tag_bytes) size = 0 for element in value: size += compute_value_size(modify_value(element)) local_EncodeVarint(write, size, deterministic) for element in value: encode_value(write, modify_value(element), deterministic) return EncodePackedField elif is_repeated: tag_bytes = TagBytes(field_number, wire_type) def EncodeRepeatedField(write, value, deterministic): for element in value: write(tag_bytes) encode_value(write, modify_value(element), deterministic) return EncodeRepeatedField else: tag_bytes = TagBytes(field_number, wire_type) def EncodeField(write, value, deterministic): write(tag_bytes) return encode_value(write, modify_value(value), deterministic) return EncodeField return SpecificEncoder def _StructPackEncoder(wire_type, format): """Return a constructor for an encoder for a fixed-width field. Args: wire_type: The field's wire type, for encoding tags. format: The format string to pass to struct.pack(). """ value_size = struct.calcsize(format) def SpecificEncoder(field_number, is_repeated, is_packed): local_struct_pack = struct.pack if is_packed: tag_bytes = TagBytes(field_number, wire_format.WIRETYPE_LENGTH_DELIMITED) local_EncodeVarint = _EncodeVarint def EncodePackedField(write, value, deterministic): write(tag_bytes) local_EncodeVarint(write, len(value) * value_size, deterministic) for element in value: write(local_struct_pack(format, element)) return EncodePackedField elif is_repeated: tag_bytes = TagBytes(field_number, wire_type) def EncodeRepeatedField(write, value, unused_deterministic=None): for element in value: write(tag_bytes) write(local_struct_pack(format, element)) return EncodeRepeatedField else: tag_bytes = TagBytes(field_number, wire_type) def EncodeField(write, value, unused_deterministic=None): write(tag_bytes) return write(local_struct_pack(format, value)) return EncodeField return SpecificEncoder def _FloatingPointEncoder(wire_type, format): """Return a constructor for an encoder for float fields. This is like StructPackEncoder, but catches errors that may be due to passing non-finite floating-point values to struct.pack, and makes a second attempt to encode those values. Args: wire_type: The field's wire type, for encoding tags. format: The format string to pass to struct.pack(). """ value_size = struct.calcsize(format) if value_size == 4: def EncodeNonFiniteOrRaise(write, value): # Remember that the serialized form uses little-endian byte order. if value == _POS_INF: write(b'\x00\x00\x80\x7F') elif value == _NEG_INF: write(b'\x00\x00\x80\xFF') elif value != value: # NaN write(b'\x00\x00\xC0\x7F') else: raise elif value_size == 8: def EncodeNonFiniteOrRaise(write, value): if value == _POS_INF: write(b'\x00\x00\x00\x00\x00\x00\xF0\x7F') elif value == _NEG_INF: write(b'\x00\x00\x00\x00\x00\x00\xF0\xFF') elif value != value: # NaN write(b'\x00\x00\x00\x00\x00\x00\xF8\x7F') else: raise else: raise ValueError('Can\'t encode floating-point values that are ' '%d bytes long (only 4 or 8)' % value_size) def SpecificEncoder(field_number, is_repeated, is_packed): local_struct_pack = struct.pack if is_packed: tag_bytes = TagBytes(field_number, wire_format.WIRETYPE_LENGTH_DELIMITED) local_EncodeVarint = _EncodeVarint def EncodePackedField(write, value, deterministic): write(tag_bytes) local_EncodeVarint(write, len(value) * value_size, deterministic) for element in value: # This try/except block is going to be faster than any code that # we could write to check whether element is finite. try: write(local_struct_pack(format, element)) except SystemError: EncodeNonFiniteOrRaise(write, element) return EncodePackedField elif is_repeated: tag_bytes = TagBytes(field_number, wire_type) def EncodeRepeatedField(write, value, unused_deterministic=None): for element in value: write(tag_bytes) try: write(local_struct_pack(format, element)) except SystemError: EncodeNonFiniteOrRaise(write, element) return EncodeRepeatedField else: tag_bytes = TagBytes(field_number, wire_type) def EncodeField(write, value, unused_deterministic=None): write(tag_bytes) try: write(local_struct_pack(format, value)) except SystemError: EncodeNonFiniteOrRaise(write, value) return EncodeField return SpecificEncoder # ==================================================================== # Here we declare an encoder constructor for each field type. These work # very similarly to sizer constructors, described earlier. Int32Encoder = Int64Encoder = EnumEncoder = _SimpleEncoder( wire_format.WIRETYPE_VARINT, _EncodeSignedVarint, _SignedVarintSize) UInt32Encoder = UInt64Encoder = _SimpleEncoder( wire_format.WIRETYPE_VARINT, _EncodeVarint, _VarintSize) SInt32Encoder = SInt64Encoder = _ModifiedEncoder( wire_format.WIRETYPE_VARINT, _EncodeVarint, _VarintSize, wire_format.ZigZagEncode) # Note that Python conveniently guarantees that when using the '<' prefix on # formats, they will also have the same size across all platforms (as opposed # to without the prefix, where their sizes depend on the C compiler's basic # type sizes). Fixed32Encoder = _StructPackEncoder(wire_format.WIRETYPE_FIXED32, ' str ValueType = int def __init__(self, enum_type): """Inits EnumTypeWrapper with an EnumDescriptor.""" self._enum_type = enum_type self.DESCRIPTOR = enum_type # pylint: disable=invalid-name def Name(self, number): # pylint: disable=invalid-name """Returns a string containing the name of an enum value.""" try: return self._enum_type.values_by_number[number].name except KeyError: pass # fall out to break exception chaining if not isinstance(number, int): raise TypeError( 'Enum value for {} must be an int, but got {} {!r}.'.format( self._enum_type.name, type(number), number)) else: # repr here to handle the odd case when you pass in a boolean. raise ValueError('Enum {} has no name defined for value {!r}'.format( self._enum_type.name, number)) def Value(self, name): # pylint: disable=invalid-name """Returns the value corresponding to the given enum name.""" try: return self._enum_type.values_by_name[name].number except KeyError: pass # fall out to break exception chaining raise ValueError('Enum {} has no value defined for name {!r}'.format( self._enum_type.name, name)) def keys(self): """Return a list of the string names in the enum. Returns: A list of strs, in the order they were defined in the .proto file. """ return [value_descriptor.name for value_descriptor in self._enum_type.values] def values(self): """Return a list of the integer values in the enum. Returns: A list of ints, in the order they were defined in the .proto file. """ return [value_descriptor.number for value_descriptor in self._enum_type.values] def items(self): """Return a list of the (name, value) pairs of the enum. Returns: A list of (str, int) pairs, in the order they were defined in the .proto file. """ return [(value_descriptor.name, value_descriptor.number) for value_descriptor in self._enum_type.values] def __getattr__(self, name): """Returns the value corresponding to the given enum name.""" try: return super( EnumTypeWrapper, self).__getattribute__('_enum_type').values_by_name[name].number except KeyError: pass # fall out to break exception chaining raise AttributeError('Enum {} has no value defined for name {!r}'.format( self._enum_type.name, name)) ================================================ FILE: openpype/hosts/nuke/vendor/google/protobuf/internal/extension_dict.py ================================================ # Protocol Buffers - Google's data interchange format # Copyright 2008 Google Inc. All rights reserved. # https://developers.google.com/protocol-buffers/ # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are # met: # # * Redistributions of source code must retain the above copyright # notice, this list of conditions and the following disclaimer. # * Redistributions in binary form must reproduce the above # copyright notice, this list of conditions and the following disclaimer # in the documentation and/or other materials provided with the # distribution. # * Neither the name of Google Inc. nor the names of its # contributors may be used to endorse or promote products derived from # this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. """Contains _ExtensionDict class to represent extensions. """ from google.protobuf.internal import type_checkers from google.protobuf.descriptor import FieldDescriptor def _VerifyExtensionHandle(message, extension_handle): """Verify that the given extension handle is valid.""" if not isinstance(extension_handle, FieldDescriptor): raise KeyError('HasExtension() expects an extension handle, got: %s' % extension_handle) if not extension_handle.is_extension: raise KeyError('"%s" is not an extension.' % extension_handle.full_name) if not extension_handle.containing_type: raise KeyError('"%s" is missing a containing_type.' % extension_handle.full_name) if extension_handle.containing_type is not message.DESCRIPTOR: raise KeyError('Extension "%s" extends message type "%s", but this ' 'message is of type "%s".' % (extension_handle.full_name, extension_handle.containing_type.full_name, message.DESCRIPTOR.full_name)) # TODO(robinson): Unify error handling of "unknown extension" crap. # TODO(robinson): Support iteritems()-style iteration over all # extensions with the "has" bits turned on? class _ExtensionDict(object): """Dict-like container for Extension fields on proto instances. Note that in all cases we expect extension handles to be FieldDescriptors. """ def __init__(self, extended_message): """ Args: extended_message: Message instance for which we are the Extensions dict. """ self._extended_message = extended_message def __getitem__(self, extension_handle): """Returns the current value of the given extension handle.""" _VerifyExtensionHandle(self._extended_message, extension_handle) result = self._extended_message._fields.get(extension_handle) if result is not None: return result if extension_handle.label == FieldDescriptor.LABEL_REPEATED: result = extension_handle._default_constructor(self._extended_message) elif extension_handle.cpp_type == FieldDescriptor.CPPTYPE_MESSAGE: message_type = extension_handle.message_type if not hasattr(message_type, '_concrete_class'): # pylint: disable=protected-access self._extended_message._FACTORY.GetPrototype(message_type) assert getattr(extension_handle.message_type, '_concrete_class', None), ( 'Uninitialized concrete class found for field %r (message type %r)' % (extension_handle.full_name, extension_handle.message_type.full_name)) result = extension_handle.message_type._concrete_class() try: result._SetListener(self._extended_message._listener_for_children) except ReferenceError: pass else: # Singular scalar -- just return the default without inserting into the # dict. return extension_handle.default_value # Atomically check if another thread has preempted us and, if not, swap # in the new object we just created. If someone has preempted us, we # take that object and discard ours. # WARNING: We are relying on setdefault() being atomic. This is true # in CPython but we haven't investigated others. This warning appears # in several other locations in this file. result = self._extended_message._fields.setdefault( extension_handle, result) return result def __eq__(self, other): if not isinstance(other, self.__class__): return False my_fields = self._extended_message.ListFields() other_fields = other._extended_message.ListFields() # Get rid of non-extension fields. my_fields = [field for field in my_fields if field.is_extension] other_fields = [field for field in other_fields if field.is_extension] return my_fields == other_fields def __ne__(self, other): return not self == other def __len__(self): fields = self._extended_message.ListFields() # Get rid of non-extension fields. extension_fields = [field for field in fields if field[0].is_extension] return len(extension_fields) def __hash__(self): raise TypeError('unhashable object') # Note that this is only meaningful for non-repeated, scalar extension # fields. Note also that we may have to call _Modified() when we do # successfully set a field this way, to set any necessary "has" bits in the # ancestors of the extended message. def __setitem__(self, extension_handle, value): """If extension_handle specifies a non-repeated, scalar extension field, sets the value of that field. """ _VerifyExtensionHandle(self._extended_message, extension_handle) if (extension_handle.label == FieldDescriptor.LABEL_REPEATED or extension_handle.cpp_type == FieldDescriptor.CPPTYPE_MESSAGE): raise TypeError( 'Cannot assign to extension "%s" because it is a repeated or ' 'composite type.' % extension_handle.full_name) # It's slightly wasteful to lookup the type checker each time, # but we expect this to be a vanishingly uncommon case anyway. type_checker = type_checkers.GetTypeChecker(extension_handle) # pylint: disable=protected-access self._extended_message._fields[extension_handle] = ( type_checker.CheckValue(value)) self._extended_message._Modified() def __delitem__(self, extension_handle): self._extended_message.ClearExtension(extension_handle) def _FindExtensionByName(self, name): """Tries to find a known extension with the specified name. Args: name: Extension full name. Returns: Extension field descriptor. """ return self._extended_message._extensions_by_name.get(name, None) def _FindExtensionByNumber(self, number): """Tries to find a known extension with the field number. Args: number: Extension field number. Returns: Extension field descriptor. """ return self._extended_message._extensions_by_number.get(number, None) def __iter__(self): # Return a generator over the populated extension fields return (f[0] for f in self._extended_message.ListFields() if f[0].is_extension) def __contains__(self, extension_handle): _VerifyExtensionHandle(self._extended_message, extension_handle) if extension_handle not in self._extended_message._fields: return False if extension_handle.label == FieldDescriptor.LABEL_REPEATED: return bool(self._extended_message._fields.get(extension_handle)) if extension_handle.cpp_type == FieldDescriptor.CPPTYPE_MESSAGE: value = self._extended_message._fields.get(extension_handle) # pylint: disable=protected-access return value is not None and value._is_present_in_parent return True ================================================ FILE: openpype/hosts/nuke/vendor/google/protobuf/internal/message_listener.py ================================================ # Protocol Buffers - Google's data interchange format # Copyright 2008 Google Inc. All rights reserved. # https://developers.google.com/protocol-buffers/ # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are # met: # # * Redistributions of source code must retain the above copyright # notice, this list of conditions and the following disclaimer. # * Redistributions in binary form must reproduce the above # copyright notice, this list of conditions and the following disclaimer # in the documentation and/or other materials provided with the # distribution. # * Neither the name of Google Inc. nor the names of its # contributors may be used to endorse or promote products derived from # this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. """Defines a listener interface for observing certain state transitions on Message objects. Also defines a null implementation of this interface. """ __author__ = 'robinson@google.com (Will Robinson)' class MessageListener(object): """Listens for modifications made to a message. Meant to be registered via Message._SetListener(). Attributes: dirty: If True, then calling Modified() would be a no-op. This can be used to avoid these calls entirely in the common case. """ def Modified(self): """Called every time the message is modified in such a way that the parent message may need to be updated. This currently means either: (a) The message was modified for the first time, so the parent message should henceforth mark the message as present. (b) The message's cached byte size became dirty -- i.e. the message was modified for the first time after a previous call to ByteSize(). Therefore the parent should also mark its byte size as dirty. Note that (a) implies (b), since new objects start out with a client cached size (zero). However, we document (a) explicitly because it is important. Modified() will *only* be called in response to one of these two events -- not every time the sub-message is modified. Note that if the listener's |dirty| attribute is true, then calling Modified at the moment would be a no-op, so it can be skipped. Performance- sensitive callers should check this attribute directly before calling since it will be true most of the time. """ raise NotImplementedError class NullMessageListener(object): """No-op MessageListener implementation.""" def Modified(self): pass ================================================ FILE: openpype/hosts/nuke/vendor/google/protobuf/internal/message_set_extensions_pb2.py ================================================ # -*- coding: utf-8 -*- # Generated by the protocol buffer compiler. DO NOT EDIT! # source: google/protobuf/internal/message_set_extensions.proto """Generated protocol buffer code.""" from google.protobuf.internal import builder as _builder from google.protobuf import descriptor as _descriptor from google.protobuf import descriptor_pool as _descriptor_pool from google.protobuf import symbol_database as _symbol_database # @@protoc_insertion_point(imports) _sym_db = _symbol_database.Default() DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n5google/protobuf/internal/message_set_extensions.proto\x12\x18google.protobuf.internal\"\x1e\n\x0eTestMessageSet*\x08\x08\x04\x10\xff\xff\xff\xff\x07:\x02\x08\x01\"\xa5\x01\n\x18TestMessageSetExtension1\x12\t\n\x01i\x18\x0f \x01(\x05\x32~\n\x15message_set_extension\x12(.google.protobuf.internal.TestMessageSet\x18\xab\xff\xf6. \x01(\x0b\x32\x32.google.protobuf.internal.TestMessageSetExtension1\"\xa7\x01\n\x18TestMessageSetExtension2\x12\x0b\n\x03str\x18\x19 \x01(\t2~\n\x15message_set_extension\x12(.google.protobuf.internal.TestMessageSet\x18\xca\xff\xf6. \x01(\x0b\x32\x32.google.protobuf.internal.TestMessageSetExtension2\"(\n\x18TestMessageSetExtension3\x12\x0c\n\x04text\x18# \x01(\t:\x7f\n\x16message_set_extension3\x12(.google.protobuf.internal.TestMessageSet\x18\xdf\xff\xf6. \x01(\x0b\x32\x32.google.protobuf.internal.TestMessageSetExtension3') _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals()) _builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'google.protobuf.internal.message_set_extensions_pb2', globals()) if _descriptor._USE_C_DESCRIPTORS == False: TestMessageSet.RegisterExtension(message_set_extension3) TestMessageSet.RegisterExtension(_TESTMESSAGESETEXTENSION1.extensions_by_name['message_set_extension']) TestMessageSet.RegisterExtension(_TESTMESSAGESETEXTENSION2.extensions_by_name['message_set_extension']) DESCRIPTOR._options = None _TESTMESSAGESET._options = None _TESTMESSAGESET._serialized_options = b'\010\001' _TESTMESSAGESET._serialized_start=83 _TESTMESSAGESET._serialized_end=113 _TESTMESSAGESETEXTENSION1._serialized_start=116 _TESTMESSAGESETEXTENSION1._serialized_end=281 _TESTMESSAGESETEXTENSION2._serialized_start=284 _TESTMESSAGESETEXTENSION2._serialized_end=451 _TESTMESSAGESETEXTENSION3._serialized_start=453 _TESTMESSAGESETEXTENSION3._serialized_end=493 # @@protoc_insertion_point(module_scope) ================================================ FILE: openpype/hosts/nuke/vendor/google/protobuf/internal/missing_enum_values_pb2.py ================================================ # -*- coding: utf-8 -*- # Generated by the protocol buffer compiler. DO NOT EDIT! # source: google/protobuf/internal/missing_enum_values.proto """Generated protocol buffer code.""" from google.protobuf.internal import builder as _builder from google.protobuf import descriptor as _descriptor from google.protobuf import descriptor_pool as _descriptor_pool from google.protobuf import symbol_database as _symbol_database # @@protoc_insertion_point(imports) _sym_db = _symbol_database.Default() DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n2google/protobuf/internal/missing_enum_values.proto\x12\x1fgoogle.protobuf.python.internal\"\xc1\x02\n\x0eTestEnumValues\x12X\n\x14optional_nested_enum\x18\x01 \x01(\x0e\x32:.google.protobuf.python.internal.TestEnumValues.NestedEnum\x12X\n\x14repeated_nested_enum\x18\x02 \x03(\x0e\x32:.google.protobuf.python.internal.TestEnumValues.NestedEnum\x12Z\n\x12packed_nested_enum\x18\x03 \x03(\x0e\x32:.google.protobuf.python.internal.TestEnumValues.NestedEnumB\x02\x10\x01\"\x1f\n\nNestedEnum\x12\x08\n\x04ZERO\x10\x00\x12\x07\n\x03ONE\x10\x01\"\xd3\x02\n\x15TestMissingEnumValues\x12_\n\x14optional_nested_enum\x18\x01 \x01(\x0e\x32\x41.google.protobuf.python.internal.TestMissingEnumValues.NestedEnum\x12_\n\x14repeated_nested_enum\x18\x02 \x03(\x0e\x32\x41.google.protobuf.python.internal.TestMissingEnumValues.NestedEnum\x12\x61\n\x12packed_nested_enum\x18\x03 \x03(\x0e\x32\x41.google.protobuf.python.internal.TestMissingEnumValues.NestedEnumB\x02\x10\x01\"\x15\n\nNestedEnum\x12\x07\n\x03TWO\x10\x02\"\x1b\n\nJustString\x12\r\n\x05\x64ummy\x18\x01 \x02(\t') _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals()) _builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'google.protobuf.internal.missing_enum_values_pb2', globals()) if _descriptor._USE_C_DESCRIPTORS == False: DESCRIPTOR._options = None _TESTENUMVALUES.fields_by_name['packed_nested_enum']._options = None _TESTENUMVALUES.fields_by_name['packed_nested_enum']._serialized_options = b'\020\001' _TESTMISSINGENUMVALUES.fields_by_name['packed_nested_enum']._options = None _TESTMISSINGENUMVALUES.fields_by_name['packed_nested_enum']._serialized_options = b'\020\001' _TESTENUMVALUES._serialized_start=88 _TESTENUMVALUES._serialized_end=409 _TESTENUMVALUES_NESTEDENUM._serialized_start=378 _TESTENUMVALUES_NESTEDENUM._serialized_end=409 _TESTMISSINGENUMVALUES._serialized_start=412 _TESTMISSINGENUMVALUES._serialized_end=751 _TESTMISSINGENUMVALUES_NESTEDENUM._serialized_start=730 _TESTMISSINGENUMVALUES_NESTEDENUM._serialized_end=751 _JUSTSTRING._serialized_start=753 _JUSTSTRING._serialized_end=780 # @@protoc_insertion_point(module_scope) ================================================ FILE: openpype/hosts/nuke/vendor/google/protobuf/internal/more_extensions_dynamic_pb2.py ================================================ # -*- coding: utf-8 -*- # Generated by the protocol buffer compiler. DO NOT EDIT! # source: google/protobuf/internal/more_extensions_dynamic.proto """Generated protocol buffer code.""" from google.protobuf.internal import builder as _builder from google.protobuf import descriptor as _descriptor from google.protobuf import descriptor_pool as _descriptor_pool from google.protobuf import symbol_database as _symbol_database # @@protoc_insertion_point(imports) _sym_db = _symbol_database.Default() from google.protobuf.internal import more_extensions_pb2 as google_dot_protobuf_dot_internal_dot_more__extensions__pb2 DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n6google/protobuf/internal/more_extensions_dynamic.proto\x12\x18google.protobuf.internal\x1a.google/protobuf/internal/more_extensions.proto\"\x1f\n\x12\x44ynamicMessageType\x12\t\n\x01\x61\x18\x01 \x01(\x05:J\n\x17\x64ynamic_int32_extension\x12).google.protobuf.internal.ExtendedMessage\x18\x64 \x01(\x05:z\n\x19\x64ynamic_message_extension\x12).google.protobuf.internal.ExtendedMessage\x18\x65 \x01(\x0b\x32,.google.protobuf.internal.DynamicMessageType:\x83\x01\n\"repeated_dynamic_message_extension\x12).google.protobuf.internal.ExtendedMessage\x18\x66 \x03(\x0b\x32,.google.protobuf.internal.DynamicMessageType') _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals()) _builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'google.protobuf.internal.more_extensions_dynamic_pb2', globals()) if _descriptor._USE_C_DESCRIPTORS == False: google_dot_protobuf_dot_internal_dot_more__extensions__pb2.ExtendedMessage.RegisterExtension(dynamic_int32_extension) google_dot_protobuf_dot_internal_dot_more__extensions__pb2.ExtendedMessage.RegisterExtension(dynamic_message_extension) google_dot_protobuf_dot_internal_dot_more__extensions__pb2.ExtendedMessage.RegisterExtension(repeated_dynamic_message_extension) DESCRIPTOR._options = None _DYNAMICMESSAGETYPE._serialized_start=132 _DYNAMICMESSAGETYPE._serialized_end=163 # @@protoc_insertion_point(module_scope) ================================================ FILE: openpype/hosts/nuke/vendor/google/protobuf/internal/more_extensions_pb2.py ================================================ # -*- coding: utf-8 -*- # Generated by the protocol buffer compiler. DO NOT EDIT! # source: google/protobuf/internal/more_extensions.proto """Generated protocol buffer code.""" from google.protobuf.internal import builder as _builder from google.protobuf import descriptor as _descriptor from google.protobuf import descriptor_pool as _descriptor_pool from google.protobuf import symbol_database as _symbol_database # @@protoc_insertion_point(imports) _sym_db = _symbol_database.Default() DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n.google/protobuf/internal/more_extensions.proto\x12\x18google.protobuf.internal\"\x99\x01\n\x0fTopLevelMessage\x12\x41\n\nsubmessage\x18\x01 \x01(\x0b\x32).google.protobuf.internal.ExtendedMessageB\x02(\x01\x12\x43\n\x0enested_message\x18\x02 \x01(\x0b\x32\'.google.protobuf.internal.NestedMessageB\x02(\x01\"R\n\rNestedMessage\x12\x41\n\nsubmessage\x18\x01 \x01(\x0b\x32).google.protobuf.internal.ExtendedMessageB\x02(\x01\"K\n\x0f\x45xtendedMessage\x12\x17\n\x0eoptional_int32\x18\xe9\x07 \x01(\x05\x12\x18\n\x0frepeated_string\x18\xea\x07 \x03(\t*\x05\x08\x01\x10\xe8\x07\"-\n\x0e\x46oreignMessage\x12\x1b\n\x13\x66oreign_message_int\x18\x01 \x01(\x05:I\n\x16optional_int_extension\x12).google.protobuf.internal.ExtendedMessage\x18\x01 \x01(\x05:w\n\x1aoptional_message_extension\x12).google.protobuf.internal.ExtendedMessage\x18\x02 \x01(\x0b\x32(.google.protobuf.internal.ForeignMessage:I\n\x16repeated_int_extension\x12).google.protobuf.internal.ExtendedMessage\x18\x03 \x03(\x05:w\n\x1arepeated_message_extension\x12).google.protobuf.internal.ExtendedMessage\x18\x04 \x03(\x0b\x32(.google.protobuf.internal.ForeignMessage') _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals()) _builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'google.protobuf.internal.more_extensions_pb2', globals()) if _descriptor._USE_C_DESCRIPTORS == False: ExtendedMessage.RegisterExtension(optional_int_extension) ExtendedMessage.RegisterExtension(optional_message_extension) ExtendedMessage.RegisterExtension(repeated_int_extension) ExtendedMessage.RegisterExtension(repeated_message_extension) DESCRIPTOR._options = None _TOPLEVELMESSAGE.fields_by_name['submessage']._options = None _TOPLEVELMESSAGE.fields_by_name['submessage']._serialized_options = b'(\001' _TOPLEVELMESSAGE.fields_by_name['nested_message']._options = None _TOPLEVELMESSAGE.fields_by_name['nested_message']._serialized_options = b'(\001' _NESTEDMESSAGE.fields_by_name['submessage']._options = None _NESTEDMESSAGE.fields_by_name['submessage']._serialized_options = b'(\001' _TOPLEVELMESSAGE._serialized_start=77 _TOPLEVELMESSAGE._serialized_end=230 _NESTEDMESSAGE._serialized_start=232 _NESTEDMESSAGE._serialized_end=314 _EXTENDEDMESSAGE._serialized_start=316 _EXTENDEDMESSAGE._serialized_end=391 _FOREIGNMESSAGE._serialized_start=393 _FOREIGNMESSAGE._serialized_end=438 # @@protoc_insertion_point(module_scope) ================================================ FILE: openpype/hosts/nuke/vendor/google/protobuf/internal/more_messages_pb2.py ================================================ # -*- coding: utf-8 -*- # Generated by the protocol buffer compiler. DO NOT EDIT! # source: google/protobuf/internal/more_messages.proto """Generated protocol buffer code.""" from google.protobuf.internal import builder as _builder from google.protobuf import descriptor as _descriptor from google.protobuf import descriptor_pool as _descriptor_pool from google.protobuf import symbol_database as _symbol_database # @@protoc_insertion_point(imports) _sym_db = _symbol_database.Default() DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n,google/protobuf/internal/more_messages.proto\x12\x18google.protobuf.internal\"h\n\x10OutOfOrderFields\x12\x17\n\x0foptional_sint32\x18\x05 \x01(\x11\x12\x17\n\x0foptional_uint32\x18\x03 \x01(\r\x12\x16\n\x0eoptional_int32\x18\x01 \x01(\x05*\x04\x08\x04\x10\x05*\x04\x08\x02\x10\x03\"\xcd\x02\n\x05\x63lass\x12\x1b\n\tint_field\x18\x01 \x01(\x05R\x08json_int\x12\n\n\x02if\x18\x02 \x01(\x05\x12(\n\x02\x61s\x18\x03 \x01(\x0e\x32\x1c.google.protobuf.internal.is\x12\x30\n\nenum_field\x18\x04 \x01(\x0e\x32\x1c.google.protobuf.internal.is\x12>\n\x11nested_enum_field\x18\x05 \x01(\x0e\x32#.google.protobuf.internal.class.for\x12;\n\x0enested_message\x18\x06 \x01(\x0b\x32#.google.protobuf.internal.class.try\x1a\x1c\n\x03try\x12\r\n\x05\x66ield\x18\x01 \x01(\x05*\x06\x08\xe7\x07\x10\x90N\"\x1c\n\x03\x66or\x12\x0b\n\x07\x64\x65\x66\x61ult\x10\x00\x12\x08\n\x04True\x10\x01*\x06\x08\xe7\x07\x10\x90N\"?\n\x0b\x45xtendClass20\n\x06return\x12\x1f.google.protobuf.internal.class\x18\xea\x07 \x01(\x05\"~\n\x0fTestFullKeyword\x12:\n\x06\x66ield1\x18\x01 \x01(\x0b\x32*.google.protobuf.internal.OutOfOrderFields\x12/\n\x06\x66ield2\x18\x02 \x01(\x0b\x32\x1f.google.protobuf.internal.class\"\xa5\x0f\n\x11LotsNestedMessage\x1a\x04\n\x02\x42\x30\x1a\x04\n\x02\x42\x31\x1a\x04\n\x02\x42\x32\x1a\x04\n\x02\x42\x33\x1a\x04\n\x02\x42\x34\x1a\x04\n\x02\x42\x35\x1a\x04\n\x02\x42\x36\x1a\x04\n\x02\x42\x37\x1a\x04\n\x02\x42\x38\x1a\x04\n\x02\x42\x39\x1a\x05\n\x03\x42\x31\x30\x1a\x05\n\x03\x42\x31\x31\x1a\x05\n\x03\x42\x31\x32\x1a\x05\n\x03\x42\x31\x33\x1a\x05\n\x03\x42\x31\x34\x1a\x05\n\x03\x42\x31\x35\x1a\x05\n\x03\x42\x31\x36\x1a\x05\n\x03\x42\x31\x37\x1a\x05\n\x03\x42\x31\x38\x1a\x05\n\x03\x42\x31\x39\x1a\x05\n\x03\x42\x32\x30\x1a\x05\n\x03\x42\x32\x31\x1a\x05\n\x03\x42\x32\x32\x1a\x05\n\x03\x42\x32\x33\x1a\x05\n\x03\x42\x32\x34\x1a\x05\n\x03\x42\x32\x35\x1a\x05\n\x03\x42\x32\x36\x1a\x05\n\x03\x42\x32\x37\x1a\x05\n\x03\x42\x32\x38\x1a\x05\n\x03\x42\x32\x39\x1a\x05\n\x03\x42\x33\x30\x1a\x05\n\x03\x42\x33\x31\x1a\x05\n\x03\x42\x33\x32\x1a\x05\n\x03\x42\x33\x33\x1a\x05\n\x03\x42\x33\x34\x1a\x05\n\x03\x42\x33\x35\x1a\x05\n\x03\x42\x33\x36\x1a\x05\n\x03\x42\x33\x37\x1a\x05\n\x03\x42\x33\x38\x1a\x05\n\x03\x42\x33\x39\x1a\x05\n\x03\x42\x34\x30\x1a\x05\n\x03\x42\x34\x31\x1a\x05\n\x03\x42\x34\x32\x1a\x05\n\x03\x42\x34\x33\x1a\x05\n\x03\x42\x34\x34\x1a\x05\n\x03\x42\x34\x35\x1a\x05\n\x03\x42\x34\x36\x1a\x05\n\x03\x42\x34\x37\x1a\x05\n\x03\x42\x34\x38\x1a\x05\n\x03\x42\x34\x39\x1a\x05\n\x03\x42\x35\x30\x1a\x05\n\x03\x42\x35\x31\x1a\x05\n\x03\x42\x35\x32\x1a\x05\n\x03\x42\x35\x33\x1a\x05\n\x03\x42\x35\x34\x1a\x05\n\x03\x42\x35\x35\x1a\x05\n\x03\x42\x35\x36\x1a\x05\n\x03\x42\x35\x37\x1a\x05\n\x03\x42\x35\x38\x1a\x05\n\x03\x42\x35\x39\x1a\x05\n\x03\x42\x36\x30\x1a\x05\n\x03\x42\x36\x31\x1a\x05\n\x03\x42\x36\x32\x1a\x05\n\x03\x42\x36\x33\x1a\x05\n\x03\x42\x36\x34\x1a\x05\n\x03\x42\x36\x35\x1a\x05\n\x03\x42\x36\x36\x1a\x05\n\x03\x42\x36\x37\x1a\x05\n\x03\x42\x36\x38\x1a\x05\n\x03\x42\x36\x39\x1a\x05\n\x03\x42\x37\x30\x1a\x05\n\x03\x42\x37\x31\x1a\x05\n\x03\x42\x37\x32\x1a\x05\n\x03\x42\x37\x33\x1a\x05\n\x03\x42\x37\x34\x1a\x05\n\x03\x42\x37\x35\x1a\x05\n\x03\x42\x37\x36\x1a\x05\n\x03\x42\x37\x37\x1a\x05\n\x03\x42\x37\x38\x1a\x05\n\x03\x42\x37\x39\x1a\x05\n\x03\x42\x38\x30\x1a\x05\n\x03\x42\x38\x31\x1a\x05\n\x03\x42\x38\x32\x1a\x05\n\x03\x42\x38\x33\x1a\x05\n\x03\x42\x38\x34\x1a\x05\n\x03\x42\x38\x35\x1a\x05\n\x03\x42\x38\x36\x1a\x05\n\x03\x42\x38\x37\x1a\x05\n\x03\x42\x38\x38\x1a\x05\n\x03\x42\x38\x39\x1a\x05\n\x03\x42\x39\x30\x1a\x05\n\x03\x42\x39\x31\x1a\x05\n\x03\x42\x39\x32\x1a\x05\n\x03\x42\x39\x33\x1a\x05\n\x03\x42\x39\x34\x1a\x05\n\x03\x42\x39\x35\x1a\x05\n\x03\x42\x39\x36\x1a\x05\n\x03\x42\x39\x37\x1a\x05\n\x03\x42\x39\x38\x1a\x05\n\x03\x42\x39\x39\x1a\x06\n\x04\x42\x31\x30\x30\x1a\x06\n\x04\x42\x31\x30\x31\x1a\x06\n\x04\x42\x31\x30\x32\x1a\x06\n\x04\x42\x31\x30\x33\x1a\x06\n\x04\x42\x31\x30\x34\x1a\x06\n\x04\x42\x31\x30\x35\x1a\x06\n\x04\x42\x31\x30\x36\x1a\x06\n\x04\x42\x31\x30\x37\x1a\x06\n\x04\x42\x31\x30\x38\x1a\x06\n\x04\x42\x31\x30\x39\x1a\x06\n\x04\x42\x31\x31\x30\x1a\x06\n\x04\x42\x31\x31\x31\x1a\x06\n\x04\x42\x31\x31\x32\x1a\x06\n\x04\x42\x31\x31\x33\x1a\x06\n\x04\x42\x31\x31\x34\x1a\x06\n\x04\x42\x31\x31\x35\x1a\x06\n\x04\x42\x31\x31\x36\x1a\x06\n\x04\x42\x31\x31\x37\x1a\x06\n\x04\x42\x31\x31\x38\x1a\x06\n\x04\x42\x31\x31\x39\x1a\x06\n\x04\x42\x31\x32\x30\x1a\x06\n\x04\x42\x31\x32\x31\x1a\x06\n\x04\x42\x31\x32\x32\x1a\x06\n\x04\x42\x31\x32\x33\x1a\x06\n\x04\x42\x31\x32\x34\x1a\x06\n\x04\x42\x31\x32\x35\x1a\x06\n\x04\x42\x31\x32\x36\x1a\x06\n\x04\x42\x31\x32\x37\x1a\x06\n\x04\x42\x31\x32\x38\x1a\x06\n\x04\x42\x31\x32\x39\x1a\x06\n\x04\x42\x31\x33\x30\x1a\x06\n\x04\x42\x31\x33\x31\x1a\x06\n\x04\x42\x31\x33\x32\x1a\x06\n\x04\x42\x31\x33\x33\x1a\x06\n\x04\x42\x31\x33\x34\x1a\x06\n\x04\x42\x31\x33\x35\x1a\x06\n\x04\x42\x31\x33\x36\x1a\x06\n\x04\x42\x31\x33\x37\x1a\x06\n\x04\x42\x31\x33\x38\x1a\x06\n\x04\x42\x31\x33\x39\x1a\x06\n\x04\x42\x31\x34\x30\x1a\x06\n\x04\x42\x31\x34\x31\x1a\x06\n\x04\x42\x31\x34\x32\x1a\x06\n\x04\x42\x31\x34\x33\x1a\x06\n\x04\x42\x31\x34\x34\x1a\x06\n\x04\x42\x31\x34\x35\x1a\x06\n\x04\x42\x31\x34\x36\x1a\x06\n\x04\x42\x31\x34\x37\x1a\x06\n\x04\x42\x31\x34\x38\x1a\x06\n\x04\x42\x31\x34\x39\x1a\x06\n\x04\x42\x31\x35\x30\x1a\x06\n\x04\x42\x31\x35\x31\x1a\x06\n\x04\x42\x31\x35\x32\x1a\x06\n\x04\x42\x31\x35\x33\x1a\x06\n\x04\x42\x31\x35\x34\x1a\x06\n\x04\x42\x31\x35\x35\x1a\x06\n\x04\x42\x31\x35\x36\x1a\x06\n\x04\x42\x31\x35\x37\x1a\x06\n\x04\x42\x31\x35\x38\x1a\x06\n\x04\x42\x31\x35\x39\x1a\x06\n\x04\x42\x31\x36\x30\x1a\x06\n\x04\x42\x31\x36\x31\x1a\x06\n\x04\x42\x31\x36\x32\x1a\x06\n\x04\x42\x31\x36\x33\x1a\x06\n\x04\x42\x31\x36\x34\x1a\x06\n\x04\x42\x31\x36\x35\x1a\x06\n\x04\x42\x31\x36\x36\x1a\x06\n\x04\x42\x31\x36\x37\x1a\x06\n\x04\x42\x31\x36\x38\x1a\x06\n\x04\x42\x31\x36\x39\x1a\x06\n\x04\x42\x31\x37\x30\x1a\x06\n\x04\x42\x31\x37\x31\x1a\x06\n\x04\x42\x31\x37\x32\x1a\x06\n\x04\x42\x31\x37\x33\x1a\x06\n\x04\x42\x31\x37\x34\x1a\x06\n\x04\x42\x31\x37\x35\x1a\x06\n\x04\x42\x31\x37\x36\x1a\x06\n\x04\x42\x31\x37\x37\x1a\x06\n\x04\x42\x31\x37\x38\x1a\x06\n\x04\x42\x31\x37\x39\x1a\x06\n\x04\x42\x31\x38\x30\x1a\x06\n\x04\x42\x31\x38\x31\x1a\x06\n\x04\x42\x31\x38\x32\x1a\x06\n\x04\x42\x31\x38\x33\x1a\x06\n\x04\x42\x31\x38\x34\x1a\x06\n\x04\x42\x31\x38\x35\x1a\x06\n\x04\x42\x31\x38\x36\x1a\x06\n\x04\x42\x31\x38\x37\x1a\x06\n\x04\x42\x31\x38\x38\x1a\x06\n\x04\x42\x31\x38\x39\x1a\x06\n\x04\x42\x31\x39\x30\x1a\x06\n\x04\x42\x31\x39\x31\x1a\x06\n\x04\x42\x31\x39\x32\x1a\x06\n\x04\x42\x31\x39\x33\x1a\x06\n\x04\x42\x31\x39\x34\x1a\x06\n\x04\x42\x31\x39\x35\x1a\x06\n\x04\x42\x31\x39\x36\x1a\x06\n\x04\x42\x31\x39\x37\x1a\x06\n\x04\x42\x31\x39\x38\x1a\x06\n\x04\x42\x31\x39\x39\x1a\x06\n\x04\x42\x32\x30\x30\x1a\x06\n\x04\x42\x32\x30\x31\x1a\x06\n\x04\x42\x32\x30\x32\x1a\x06\n\x04\x42\x32\x30\x33\x1a\x06\n\x04\x42\x32\x30\x34\x1a\x06\n\x04\x42\x32\x30\x35\x1a\x06\n\x04\x42\x32\x30\x36\x1a\x06\n\x04\x42\x32\x30\x37\x1a\x06\n\x04\x42\x32\x30\x38\x1a\x06\n\x04\x42\x32\x30\x39\x1a\x06\n\x04\x42\x32\x31\x30\x1a\x06\n\x04\x42\x32\x31\x31\x1a\x06\n\x04\x42\x32\x31\x32\x1a\x06\n\x04\x42\x32\x31\x33\x1a\x06\n\x04\x42\x32\x31\x34\x1a\x06\n\x04\x42\x32\x31\x35\x1a\x06\n\x04\x42\x32\x31\x36\x1a\x06\n\x04\x42\x32\x31\x37\x1a\x06\n\x04\x42\x32\x31\x38\x1a\x06\n\x04\x42\x32\x31\x39\x1a\x06\n\x04\x42\x32\x32\x30\x1a\x06\n\x04\x42\x32\x32\x31\x1a\x06\n\x04\x42\x32\x32\x32\x1a\x06\n\x04\x42\x32\x32\x33\x1a\x06\n\x04\x42\x32\x32\x34\x1a\x06\n\x04\x42\x32\x32\x35\x1a\x06\n\x04\x42\x32\x32\x36\x1a\x06\n\x04\x42\x32\x32\x37\x1a\x06\n\x04\x42\x32\x32\x38\x1a\x06\n\x04\x42\x32\x32\x39\x1a\x06\n\x04\x42\x32\x33\x30\x1a\x06\n\x04\x42\x32\x33\x31\x1a\x06\n\x04\x42\x32\x33\x32\x1a\x06\n\x04\x42\x32\x33\x33\x1a\x06\n\x04\x42\x32\x33\x34\x1a\x06\n\x04\x42\x32\x33\x35\x1a\x06\n\x04\x42\x32\x33\x36\x1a\x06\n\x04\x42\x32\x33\x37\x1a\x06\n\x04\x42\x32\x33\x38\x1a\x06\n\x04\x42\x32\x33\x39\x1a\x06\n\x04\x42\x32\x34\x30\x1a\x06\n\x04\x42\x32\x34\x31\x1a\x06\n\x04\x42\x32\x34\x32\x1a\x06\n\x04\x42\x32\x34\x33\x1a\x06\n\x04\x42\x32\x34\x34\x1a\x06\n\x04\x42\x32\x34\x35\x1a\x06\n\x04\x42\x32\x34\x36\x1a\x06\n\x04\x42\x32\x34\x37\x1a\x06\n\x04\x42\x32\x34\x38\x1a\x06\n\x04\x42\x32\x34\x39\x1a\x06\n\x04\x42\x32\x35\x30\x1a\x06\n\x04\x42\x32\x35\x31\x1a\x06\n\x04\x42\x32\x35\x32\x1a\x06\n\x04\x42\x32\x35\x33\x1a\x06\n\x04\x42\x32\x35\x34\x1a\x06\n\x04\x42\x32\x35\x35*\x1b\n\x02is\x12\x0b\n\x07\x64\x65\x66\x61ult\x10\x00\x12\x08\n\x04\x65lse\x10\x01:C\n\x0foptional_uint64\x12*.google.protobuf.internal.OutOfOrderFields\x18\x04 \x01(\x04:B\n\x0eoptional_int64\x12*.google.protobuf.internal.OutOfOrderFields\x18\x02 \x01(\x03:2\n\x08\x63ontinue\x12\x1f.google.protobuf.internal.class\x18\xe9\x07 \x01(\x05:2\n\x04with\x12#.google.protobuf.internal.class.try\x18\xe9\x07 \x01(\x05') _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals()) _builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'google.protobuf.internal.more_messages_pb2', globals()) if _descriptor._USE_C_DESCRIPTORS == False: OutOfOrderFields.RegisterExtension(optional_uint64) OutOfOrderFields.RegisterExtension(optional_int64) globals()['class'].RegisterExtension(globals()['continue']) getattr(globals()['class'], 'try').RegisterExtension(globals()['with']) globals()['class'].RegisterExtension(_EXTENDCLASS.extensions_by_name['return']) DESCRIPTOR._options = None _IS._serialized_start=2669 _IS._serialized_end=2696 _OUTOFORDERFIELDS._serialized_start=74 _OUTOFORDERFIELDS._serialized_end=178 _CLASS._serialized_start=181 _CLASS._serialized_end=514 _CLASS_TRY._serialized_start=448 _CLASS_TRY._serialized_end=476 _CLASS_FOR._serialized_start=478 _CLASS_FOR._serialized_end=506 _EXTENDCLASS._serialized_start=516 _EXTENDCLASS._serialized_end=579 _TESTFULLKEYWORD._serialized_start=581 _TESTFULLKEYWORD._serialized_end=707 _LOTSNESTEDMESSAGE._serialized_start=710 _LOTSNESTEDMESSAGE._serialized_end=2667 _LOTSNESTEDMESSAGE_B0._serialized_start=731 _LOTSNESTEDMESSAGE_B0._serialized_end=735 _LOTSNESTEDMESSAGE_B1._serialized_start=737 _LOTSNESTEDMESSAGE_B1._serialized_end=741 _LOTSNESTEDMESSAGE_B2._serialized_start=743 _LOTSNESTEDMESSAGE_B2._serialized_end=747 _LOTSNESTEDMESSAGE_B3._serialized_start=749 _LOTSNESTEDMESSAGE_B3._serialized_end=753 _LOTSNESTEDMESSAGE_B4._serialized_start=755 _LOTSNESTEDMESSAGE_B4._serialized_end=759 _LOTSNESTEDMESSAGE_B5._serialized_start=761 _LOTSNESTEDMESSAGE_B5._serialized_end=765 _LOTSNESTEDMESSAGE_B6._serialized_start=767 _LOTSNESTEDMESSAGE_B6._serialized_end=771 _LOTSNESTEDMESSAGE_B7._serialized_start=773 _LOTSNESTEDMESSAGE_B7._serialized_end=777 _LOTSNESTEDMESSAGE_B8._serialized_start=779 _LOTSNESTEDMESSAGE_B8._serialized_end=783 _LOTSNESTEDMESSAGE_B9._serialized_start=785 _LOTSNESTEDMESSAGE_B9._serialized_end=789 _LOTSNESTEDMESSAGE_B10._serialized_start=791 _LOTSNESTEDMESSAGE_B10._serialized_end=796 _LOTSNESTEDMESSAGE_B11._serialized_start=798 _LOTSNESTEDMESSAGE_B11._serialized_end=803 _LOTSNESTEDMESSAGE_B12._serialized_start=805 _LOTSNESTEDMESSAGE_B12._serialized_end=810 _LOTSNESTEDMESSAGE_B13._serialized_start=812 _LOTSNESTEDMESSAGE_B13._serialized_end=817 _LOTSNESTEDMESSAGE_B14._serialized_start=819 _LOTSNESTEDMESSAGE_B14._serialized_end=824 _LOTSNESTEDMESSAGE_B15._serialized_start=826 _LOTSNESTEDMESSAGE_B15._serialized_end=831 _LOTSNESTEDMESSAGE_B16._serialized_start=833 _LOTSNESTEDMESSAGE_B16._serialized_end=838 _LOTSNESTEDMESSAGE_B17._serialized_start=840 _LOTSNESTEDMESSAGE_B17._serialized_end=845 _LOTSNESTEDMESSAGE_B18._serialized_start=847 _LOTSNESTEDMESSAGE_B18._serialized_end=852 _LOTSNESTEDMESSAGE_B19._serialized_start=854 _LOTSNESTEDMESSAGE_B19._serialized_end=859 _LOTSNESTEDMESSAGE_B20._serialized_start=861 _LOTSNESTEDMESSAGE_B20._serialized_end=866 _LOTSNESTEDMESSAGE_B21._serialized_start=868 _LOTSNESTEDMESSAGE_B21._serialized_end=873 _LOTSNESTEDMESSAGE_B22._serialized_start=875 _LOTSNESTEDMESSAGE_B22._serialized_end=880 _LOTSNESTEDMESSAGE_B23._serialized_start=882 _LOTSNESTEDMESSAGE_B23._serialized_end=887 _LOTSNESTEDMESSAGE_B24._serialized_start=889 _LOTSNESTEDMESSAGE_B24._serialized_end=894 _LOTSNESTEDMESSAGE_B25._serialized_start=896 _LOTSNESTEDMESSAGE_B25._serialized_end=901 _LOTSNESTEDMESSAGE_B26._serialized_start=903 _LOTSNESTEDMESSAGE_B26._serialized_end=908 _LOTSNESTEDMESSAGE_B27._serialized_start=910 _LOTSNESTEDMESSAGE_B27._serialized_end=915 _LOTSNESTEDMESSAGE_B28._serialized_start=917 _LOTSNESTEDMESSAGE_B28._serialized_end=922 _LOTSNESTEDMESSAGE_B29._serialized_start=924 _LOTSNESTEDMESSAGE_B29._serialized_end=929 _LOTSNESTEDMESSAGE_B30._serialized_start=931 _LOTSNESTEDMESSAGE_B30._serialized_end=936 _LOTSNESTEDMESSAGE_B31._serialized_start=938 _LOTSNESTEDMESSAGE_B31._serialized_end=943 _LOTSNESTEDMESSAGE_B32._serialized_start=945 _LOTSNESTEDMESSAGE_B32._serialized_end=950 _LOTSNESTEDMESSAGE_B33._serialized_start=952 _LOTSNESTEDMESSAGE_B33._serialized_end=957 _LOTSNESTEDMESSAGE_B34._serialized_start=959 _LOTSNESTEDMESSAGE_B34._serialized_end=964 _LOTSNESTEDMESSAGE_B35._serialized_start=966 _LOTSNESTEDMESSAGE_B35._serialized_end=971 _LOTSNESTEDMESSAGE_B36._serialized_start=973 _LOTSNESTEDMESSAGE_B36._serialized_end=978 _LOTSNESTEDMESSAGE_B37._serialized_start=980 _LOTSNESTEDMESSAGE_B37._serialized_end=985 _LOTSNESTEDMESSAGE_B38._serialized_start=987 _LOTSNESTEDMESSAGE_B38._serialized_end=992 _LOTSNESTEDMESSAGE_B39._serialized_start=994 _LOTSNESTEDMESSAGE_B39._serialized_end=999 _LOTSNESTEDMESSAGE_B40._serialized_start=1001 _LOTSNESTEDMESSAGE_B40._serialized_end=1006 _LOTSNESTEDMESSAGE_B41._serialized_start=1008 _LOTSNESTEDMESSAGE_B41._serialized_end=1013 _LOTSNESTEDMESSAGE_B42._serialized_start=1015 _LOTSNESTEDMESSAGE_B42._serialized_end=1020 _LOTSNESTEDMESSAGE_B43._serialized_start=1022 _LOTSNESTEDMESSAGE_B43._serialized_end=1027 _LOTSNESTEDMESSAGE_B44._serialized_start=1029 _LOTSNESTEDMESSAGE_B44._serialized_end=1034 _LOTSNESTEDMESSAGE_B45._serialized_start=1036 _LOTSNESTEDMESSAGE_B45._serialized_end=1041 _LOTSNESTEDMESSAGE_B46._serialized_start=1043 _LOTSNESTEDMESSAGE_B46._serialized_end=1048 _LOTSNESTEDMESSAGE_B47._serialized_start=1050 _LOTSNESTEDMESSAGE_B47._serialized_end=1055 _LOTSNESTEDMESSAGE_B48._serialized_start=1057 _LOTSNESTEDMESSAGE_B48._serialized_end=1062 _LOTSNESTEDMESSAGE_B49._serialized_start=1064 _LOTSNESTEDMESSAGE_B49._serialized_end=1069 _LOTSNESTEDMESSAGE_B50._serialized_start=1071 _LOTSNESTEDMESSAGE_B50._serialized_end=1076 _LOTSNESTEDMESSAGE_B51._serialized_start=1078 _LOTSNESTEDMESSAGE_B51._serialized_end=1083 _LOTSNESTEDMESSAGE_B52._serialized_start=1085 _LOTSNESTEDMESSAGE_B52._serialized_end=1090 _LOTSNESTEDMESSAGE_B53._serialized_start=1092 _LOTSNESTEDMESSAGE_B53._serialized_end=1097 _LOTSNESTEDMESSAGE_B54._serialized_start=1099 _LOTSNESTEDMESSAGE_B54._serialized_end=1104 _LOTSNESTEDMESSAGE_B55._serialized_start=1106 _LOTSNESTEDMESSAGE_B55._serialized_end=1111 _LOTSNESTEDMESSAGE_B56._serialized_start=1113 _LOTSNESTEDMESSAGE_B56._serialized_end=1118 _LOTSNESTEDMESSAGE_B57._serialized_start=1120 _LOTSNESTEDMESSAGE_B57._serialized_end=1125 _LOTSNESTEDMESSAGE_B58._serialized_start=1127 _LOTSNESTEDMESSAGE_B58._serialized_end=1132 _LOTSNESTEDMESSAGE_B59._serialized_start=1134 _LOTSNESTEDMESSAGE_B59._serialized_end=1139 _LOTSNESTEDMESSAGE_B60._serialized_start=1141 _LOTSNESTEDMESSAGE_B60._serialized_end=1146 _LOTSNESTEDMESSAGE_B61._serialized_start=1148 _LOTSNESTEDMESSAGE_B61._serialized_end=1153 _LOTSNESTEDMESSAGE_B62._serialized_start=1155 _LOTSNESTEDMESSAGE_B62._serialized_end=1160 _LOTSNESTEDMESSAGE_B63._serialized_start=1162 _LOTSNESTEDMESSAGE_B63._serialized_end=1167 _LOTSNESTEDMESSAGE_B64._serialized_start=1169 _LOTSNESTEDMESSAGE_B64._serialized_end=1174 _LOTSNESTEDMESSAGE_B65._serialized_start=1176 _LOTSNESTEDMESSAGE_B65._serialized_end=1181 _LOTSNESTEDMESSAGE_B66._serialized_start=1183 _LOTSNESTEDMESSAGE_B66._serialized_end=1188 _LOTSNESTEDMESSAGE_B67._serialized_start=1190 _LOTSNESTEDMESSAGE_B67._serialized_end=1195 _LOTSNESTEDMESSAGE_B68._serialized_start=1197 _LOTSNESTEDMESSAGE_B68._serialized_end=1202 _LOTSNESTEDMESSAGE_B69._serialized_start=1204 _LOTSNESTEDMESSAGE_B69._serialized_end=1209 _LOTSNESTEDMESSAGE_B70._serialized_start=1211 _LOTSNESTEDMESSAGE_B70._serialized_end=1216 _LOTSNESTEDMESSAGE_B71._serialized_start=1218 _LOTSNESTEDMESSAGE_B71._serialized_end=1223 _LOTSNESTEDMESSAGE_B72._serialized_start=1225 _LOTSNESTEDMESSAGE_B72._serialized_end=1230 _LOTSNESTEDMESSAGE_B73._serialized_start=1232 _LOTSNESTEDMESSAGE_B73._serialized_end=1237 _LOTSNESTEDMESSAGE_B74._serialized_start=1239 _LOTSNESTEDMESSAGE_B74._serialized_end=1244 _LOTSNESTEDMESSAGE_B75._serialized_start=1246 _LOTSNESTEDMESSAGE_B75._serialized_end=1251 _LOTSNESTEDMESSAGE_B76._serialized_start=1253 _LOTSNESTEDMESSAGE_B76._serialized_end=1258 _LOTSNESTEDMESSAGE_B77._serialized_start=1260 _LOTSNESTEDMESSAGE_B77._serialized_end=1265 _LOTSNESTEDMESSAGE_B78._serialized_start=1267 _LOTSNESTEDMESSAGE_B78._serialized_end=1272 _LOTSNESTEDMESSAGE_B79._serialized_start=1274 _LOTSNESTEDMESSAGE_B79._serialized_end=1279 _LOTSNESTEDMESSAGE_B80._serialized_start=1281 _LOTSNESTEDMESSAGE_B80._serialized_end=1286 _LOTSNESTEDMESSAGE_B81._serialized_start=1288 _LOTSNESTEDMESSAGE_B81._serialized_end=1293 _LOTSNESTEDMESSAGE_B82._serialized_start=1295 _LOTSNESTEDMESSAGE_B82._serialized_end=1300 _LOTSNESTEDMESSAGE_B83._serialized_start=1302 _LOTSNESTEDMESSAGE_B83._serialized_end=1307 _LOTSNESTEDMESSAGE_B84._serialized_start=1309 _LOTSNESTEDMESSAGE_B84._serialized_end=1314 _LOTSNESTEDMESSAGE_B85._serialized_start=1316 _LOTSNESTEDMESSAGE_B85._serialized_end=1321 _LOTSNESTEDMESSAGE_B86._serialized_start=1323 _LOTSNESTEDMESSAGE_B86._serialized_end=1328 _LOTSNESTEDMESSAGE_B87._serialized_start=1330 _LOTSNESTEDMESSAGE_B87._serialized_end=1335 _LOTSNESTEDMESSAGE_B88._serialized_start=1337 _LOTSNESTEDMESSAGE_B88._serialized_end=1342 _LOTSNESTEDMESSAGE_B89._serialized_start=1344 _LOTSNESTEDMESSAGE_B89._serialized_end=1349 _LOTSNESTEDMESSAGE_B90._serialized_start=1351 _LOTSNESTEDMESSAGE_B90._serialized_end=1356 _LOTSNESTEDMESSAGE_B91._serialized_start=1358 _LOTSNESTEDMESSAGE_B91._serialized_end=1363 _LOTSNESTEDMESSAGE_B92._serialized_start=1365 _LOTSNESTEDMESSAGE_B92._serialized_end=1370 _LOTSNESTEDMESSAGE_B93._serialized_start=1372 _LOTSNESTEDMESSAGE_B93._serialized_end=1377 _LOTSNESTEDMESSAGE_B94._serialized_start=1379 _LOTSNESTEDMESSAGE_B94._serialized_end=1384 _LOTSNESTEDMESSAGE_B95._serialized_start=1386 _LOTSNESTEDMESSAGE_B95._serialized_end=1391 _LOTSNESTEDMESSAGE_B96._serialized_start=1393 _LOTSNESTEDMESSAGE_B96._serialized_end=1398 _LOTSNESTEDMESSAGE_B97._serialized_start=1400 _LOTSNESTEDMESSAGE_B97._serialized_end=1405 _LOTSNESTEDMESSAGE_B98._serialized_start=1407 _LOTSNESTEDMESSAGE_B98._serialized_end=1412 _LOTSNESTEDMESSAGE_B99._serialized_start=1414 _LOTSNESTEDMESSAGE_B99._serialized_end=1419 _LOTSNESTEDMESSAGE_B100._serialized_start=1421 _LOTSNESTEDMESSAGE_B100._serialized_end=1427 _LOTSNESTEDMESSAGE_B101._serialized_start=1429 _LOTSNESTEDMESSAGE_B101._serialized_end=1435 _LOTSNESTEDMESSAGE_B102._serialized_start=1437 _LOTSNESTEDMESSAGE_B102._serialized_end=1443 _LOTSNESTEDMESSAGE_B103._serialized_start=1445 _LOTSNESTEDMESSAGE_B103._serialized_end=1451 _LOTSNESTEDMESSAGE_B104._serialized_start=1453 _LOTSNESTEDMESSAGE_B104._serialized_end=1459 _LOTSNESTEDMESSAGE_B105._serialized_start=1461 _LOTSNESTEDMESSAGE_B105._serialized_end=1467 _LOTSNESTEDMESSAGE_B106._serialized_start=1469 _LOTSNESTEDMESSAGE_B106._serialized_end=1475 _LOTSNESTEDMESSAGE_B107._serialized_start=1477 _LOTSNESTEDMESSAGE_B107._serialized_end=1483 _LOTSNESTEDMESSAGE_B108._serialized_start=1485 _LOTSNESTEDMESSAGE_B108._serialized_end=1491 _LOTSNESTEDMESSAGE_B109._serialized_start=1493 _LOTSNESTEDMESSAGE_B109._serialized_end=1499 _LOTSNESTEDMESSAGE_B110._serialized_start=1501 _LOTSNESTEDMESSAGE_B110._serialized_end=1507 _LOTSNESTEDMESSAGE_B111._serialized_start=1509 _LOTSNESTEDMESSAGE_B111._serialized_end=1515 _LOTSNESTEDMESSAGE_B112._serialized_start=1517 _LOTSNESTEDMESSAGE_B112._serialized_end=1523 _LOTSNESTEDMESSAGE_B113._serialized_start=1525 _LOTSNESTEDMESSAGE_B113._serialized_end=1531 _LOTSNESTEDMESSAGE_B114._serialized_start=1533 _LOTSNESTEDMESSAGE_B114._serialized_end=1539 _LOTSNESTEDMESSAGE_B115._serialized_start=1541 _LOTSNESTEDMESSAGE_B115._serialized_end=1547 _LOTSNESTEDMESSAGE_B116._serialized_start=1549 _LOTSNESTEDMESSAGE_B116._serialized_end=1555 _LOTSNESTEDMESSAGE_B117._serialized_start=1557 _LOTSNESTEDMESSAGE_B117._serialized_end=1563 _LOTSNESTEDMESSAGE_B118._serialized_start=1565 _LOTSNESTEDMESSAGE_B118._serialized_end=1571 _LOTSNESTEDMESSAGE_B119._serialized_start=1573 _LOTSNESTEDMESSAGE_B119._serialized_end=1579 _LOTSNESTEDMESSAGE_B120._serialized_start=1581 _LOTSNESTEDMESSAGE_B120._serialized_end=1587 _LOTSNESTEDMESSAGE_B121._serialized_start=1589 _LOTSNESTEDMESSAGE_B121._serialized_end=1595 _LOTSNESTEDMESSAGE_B122._serialized_start=1597 _LOTSNESTEDMESSAGE_B122._serialized_end=1603 _LOTSNESTEDMESSAGE_B123._serialized_start=1605 _LOTSNESTEDMESSAGE_B123._serialized_end=1611 _LOTSNESTEDMESSAGE_B124._serialized_start=1613 _LOTSNESTEDMESSAGE_B124._serialized_end=1619 _LOTSNESTEDMESSAGE_B125._serialized_start=1621 _LOTSNESTEDMESSAGE_B125._serialized_end=1627 _LOTSNESTEDMESSAGE_B126._serialized_start=1629 _LOTSNESTEDMESSAGE_B126._serialized_end=1635 _LOTSNESTEDMESSAGE_B127._serialized_start=1637 _LOTSNESTEDMESSAGE_B127._serialized_end=1643 _LOTSNESTEDMESSAGE_B128._serialized_start=1645 _LOTSNESTEDMESSAGE_B128._serialized_end=1651 _LOTSNESTEDMESSAGE_B129._serialized_start=1653 _LOTSNESTEDMESSAGE_B129._serialized_end=1659 _LOTSNESTEDMESSAGE_B130._serialized_start=1661 _LOTSNESTEDMESSAGE_B130._serialized_end=1667 _LOTSNESTEDMESSAGE_B131._serialized_start=1669 _LOTSNESTEDMESSAGE_B131._serialized_end=1675 _LOTSNESTEDMESSAGE_B132._serialized_start=1677 _LOTSNESTEDMESSAGE_B132._serialized_end=1683 _LOTSNESTEDMESSAGE_B133._serialized_start=1685 _LOTSNESTEDMESSAGE_B133._serialized_end=1691 _LOTSNESTEDMESSAGE_B134._serialized_start=1693 _LOTSNESTEDMESSAGE_B134._serialized_end=1699 _LOTSNESTEDMESSAGE_B135._serialized_start=1701 _LOTSNESTEDMESSAGE_B135._serialized_end=1707 _LOTSNESTEDMESSAGE_B136._serialized_start=1709 _LOTSNESTEDMESSAGE_B136._serialized_end=1715 _LOTSNESTEDMESSAGE_B137._serialized_start=1717 _LOTSNESTEDMESSAGE_B137._serialized_end=1723 _LOTSNESTEDMESSAGE_B138._serialized_start=1725 _LOTSNESTEDMESSAGE_B138._serialized_end=1731 _LOTSNESTEDMESSAGE_B139._serialized_start=1733 _LOTSNESTEDMESSAGE_B139._serialized_end=1739 _LOTSNESTEDMESSAGE_B140._serialized_start=1741 _LOTSNESTEDMESSAGE_B140._serialized_end=1747 _LOTSNESTEDMESSAGE_B141._serialized_start=1749 _LOTSNESTEDMESSAGE_B141._serialized_end=1755 _LOTSNESTEDMESSAGE_B142._serialized_start=1757 _LOTSNESTEDMESSAGE_B142._serialized_end=1763 _LOTSNESTEDMESSAGE_B143._serialized_start=1765 _LOTSNESTEDMESSAGE_B143._serialized_end=1771 _LOTSNESTEDMESSAGE_B144._serialized_start=1773 _LOTSNESTEDMESSAGE_B144._serialized_end=1779 _LOTSNESTEDMESSAGE_B145._serialized_start=1781 _LOTSNESTEDMESSAGE_B145._serialized_end=1787 _LOTSNESTEDMESSAGE_B146._serialized_start=1789 _LOTSNESTEDMESSAGE_B146._serialized_end=1795 _LOTSNESTEDMESSAGE_B147._serialized_start=1797 _LOTSNESTEDMESSAGE_B147._serialized_end=1803 _LOTSNESTEDMESSAGE_B148._serialized_start=1805 _LOTSNESTEDMESSAGE_B148._serialized_end=1811 _LOTSNESTEDMESSAGE_B149._serialized_start=1813 _LOTSNESTEDMESSAGE_B149._serialized_end=1819 _LOTSNESTEDMESSAGE_B150._serialized_start=1821 _LOTSNESTEDMESSAGE_B150._serialized_end=1827 _LOTSNESTEDMESSAGE_B151._serialized_start=1829 _LOTSNESTEDMESSAGE_B151._serialized_end=1835 _LOTSNESTEDMESSAGE_B152._serialized_start=1837 _LOTSNESTEDMESSAGE_B152._serialized_end=1843 _LOTSNESTEDMESSAGE_B153._serialized_start=1845 _LOTSNESTEDMESSAGE_B153._serialized_end=1851 _LOTSNESTEDMESSAGE_B154._serialized_start=1853 _LOTSNESTEDMESSAGE_B154._serialized_end=1859 _LOTSNESTEDMESSAGE_B155._serialized_start=1861 _LOTSNESTEDMESSAGE_B155._serialized_end=1867 _LOTSNESTEDMESSAGE_B156._serialized_start=1869 _LOTSNESTEDMESSAGE_B156._serialized_end=1875 _LOTSNESTEDMESSAGE_B157._serialized_start=1877 _LOTSNESTEDMESSAGE_B157._serialized_end=1883 _LOTSNESTEDMESSAGE_B158._serialized_start=1885 _LOTSNESTEDMESSAGE_B158._serialized_end=1891 _LOTSNESTEDMESSAGE_B159._serialized_start=1893 _LOTSNESTEDMESSAGE_B159._serialized_end=1899 _LOTSNESTEDMESSAGE_B160._serialized_start=1901 _LOTSNESTEDMESSAGE_B160._serialized_end=1907 _LOTSNESTEDMESSAGE_B161._serialized_start=1909 _LOTSNESTEDMESSAGE_B161._serialized_end=1915 _LOTSNESTEDMESSAGE_B162._serialized_start=1917 _LOTSNESTEDMESSAGE_B162._serialized_end=1923 _LOTSNESTEDMESSAGE_B163._serialized_start=1925 _LOTSNESTEDMESSAGE_B163._serialized_end=1931 _LOTSNESTEDMESSAGE_B164._serialized_start=1933 _LOTSNESTEDMESSAGE_B164._serialized_end=1939 _LOTSNESTEDMESSAGE_B165._serialized_start=1941 _LOTSNESTEDMESSAGE_B165._serialized_end=1947 _LOTSNESTEDMESSAGE_B166._serialized_start=1949 _LOTSNESTEDMESSAGE_B166._serialized_end=1955 _LOTSNESTEDMESSAGE_B167._serialized_start=1957 _LOTSNESTEDMESSAGE_B167._serialized_end=1963 _LOTSNESTEDMESSAGE_B168._serialized_start=1965 _LOTSNESTEDMESSAGE_B168._serialized_end=1971 _LOTSNESTEDMESSAGE_B169._serialized_start=1973 _LOTSNESTEDMESSAGE_B169._serialized_end=1979 _LOTSNESTEDMESSAGE_B170._serialized_start=1981 _LOTSNESTEDMESSAGE_B170._serialized_end=1987 _LOTSNESTEDMESSAGE_B171._serialized_start=1989 _LOTSNESTEDMESSAGE_B171._serialized_end=1995 _LOTSNESTEDMESSAGE_B172._serialized_start=1997 _LOTSNESTEDMESSAGE_B172._serialized_end=2003 _LOTSNESTEDMESSAGE_B173._serialized_start=2005 _LOTSNESTEDMESSAGE_B173._serialized_end=2011 _LOTSNESTEDMESSAGE_B174._serialized_start=2013 _LOTSNESTEDMESSAGE_B174._serialized_end=2019 _LOTSNESTEDMESSAGE_B175._serialized_start=2021 _LOTSNESTEDMESSAGE_B175._serialized_end=2027 _LOTSNESTEDMESSAGE_B176._serialized_start=2029 _LOTSNESTEDMESSAGE_B176._serialized_end=2035 _LOTSNESTEDMESSAGE_B177._serialized_start=2037 _LOTSNESTEDMESSAGE_B177._serialized_end=2043 _LOTSNESTEDMESSAGE_B178._serialized_start=2045 _LOTSNESTEDMESSAGE_B178._serialized_end=2051 _LOTSNESTEDMESSAGE_B179._serialized_start=2053 _LOTSNESTEDMESSAGE_B179._serialized_end=2059 _LOTSNESTEDMESSAGE_B180._serialized_start=2061 _LOTSNESTEDMESSAGE_B180._serialized_end=2067 _LOTSNESTEDMESSAGE_B181._serialized_start=2069 _LOTSNESTEDMESSAGE_B181._serialized_end=2075 _LOTSNESTEDMESSAGE_B182._serialized_start=2077 _LOTSNESTEDMESSAGE_B182._serialized_end=2083 _LOTSNESTEDMESSAGE_B183._serialized_start=2085 _LOTSNESTEDMESSAGE_B183._serialized_end=2091 _LOTSNESTEDMESSAGE_B184._serialized_start=2093 _LOTSNESTEDMESSAGE_B184._serialized_end=2099 _LOTSNESTEDMESSAGE_B185._serialized_start=2101 _LOTSNESTEDMESSAGE_B185._serialized_end=2107 _LOTSNESTEDMESSAGE_B186._serialized_start=2109 _LOTSNESTEDMESSAGE_B186._serialized_end=2115 _LOTSNESTEDMESSAGE_B187._serialized_start=2117 _LOTSNESTEDMESSAGE_B187._serialized_end=2123 _LOTSNESTEDMESSAGE_B188._serialized_start=2125 _LOTSNESTEDMESSAGE_B188._serialized_end=2131 _LOTSNESTEDMESSAGE_B189._serialized_start=2133 _LOTSNESTEDMESSAGE_B189._serialized_end=2139 _LOTSNESTEDMESSAGE_B190._serialized_start=2141 _LOTSNESTEDMESSAGE_B190._serialized_end=2147 _LOTSNESTEDMESSAGE_B191._serialized_start=2149 _LOTSNESTEDMESSAGE_B191._serialized_end=2155 _LOTSNESTEDMESSAGE_B192._serialized_start=2157 _LOTSNESTEDMESSAGE_B192._serialized_end=2163 _LOTSNESTEDMESSAGE_B193._serialized_start=2165 _LOTSNESTEDMESSAGE_B193._serialized_end=2171 _LOTSNESTEDMESSAGE_B194._serialized_start=2173 _LOTSNESTEDMESSAGE_B194._serialized_end=2179 _LOTSNESTEDMESSAGE_B195._serialized_start=2181 _LOTSNESTEDMESSAGE_B195._serialized_end=2187 _LOTSNESTEDMESSAGE_B196._serialized_start=2189 _LOTSNESTEDMESSAGE_B196._serialized_end=2195 _LOTSNESTEDMESSAGE_B197._serialized_start=2197 _LOTSNESTEDMESSAGE_B197._serialized_end=2203 _LOTSNESTEDMESSAGE_B198._serialized_start=2205 _LOTSNESTEDMESSAGE_B198._serialized_end=2211 _LOTSNESTEDMESSAGE_B199._serialized_start=2213 _LOTSNESTEDMESSAGE_B199._serialized_end=2219 _LOTSNESTEDMESSAGE_B200._serialized_start=2221 _LOTSNESTEDMESSAGE_B200._serialized_end=2227 _LOTSNESTEDMESSAGE_B201._serialized_start=2229 _LOTSNESTEDMESSAGE_B201._serialized_end=2235 _LOTSNESTEDMESSAGE_B202._serialized_start=2237 _LOTSNESTEDMESSAGE_B202._serialized_end=2243 _LOTSNESTEDMESSAGE_B203._serialized_start=2245 _LOTSNESTEDMESSAGE_B203._serialized_end=2251 _LOTSNESTEDMESSAGE_B204._serialized_start=2253 _LOTSNESTEDMESSAGE_B204._serialized_end=2259 _LOTSNESTEDMESSAGE_B205._serialized_start=2261 _LOTSNESTEDMESSAGE_B205._serialized_end=2267 _LOTSNESTEDMESSAGE_B206._serialized_start=2269 _LOTSNESTEDMESSAGE_B206._serialized_end=2275 _LOTSNESTEDMESSAGE_B207._serialized_start=2277 _LOTSNESTEDMESSAGE_B207._serialized_end=2283 _LOTSNESTEDMESSAGE_B208._serialized_start=2285 _LOTSNESTEDMESSAGE_B208._serialized_end=2291 _LOTSNESTEDMESSAGE_B209._serialized_start=2293 _LOTSNESTEDMESSAGE_B209._serialized_end=2299 _LOTSNESTEDMESSAGE_B210._serialized_start=2301 _LOTSNESTEDMESSAGE_B210._serialized_end=2307 _LOTSNESTEDMESSAGE_B211._serialized_start=2309 _LOTSNESTEDMESSAGE_B211._serialized_end=2315 _LOTSNESTEDMESSAGE_B212._serialized_start=2317 _LOTSNESTEDMESSAGE_B212._serialized_end=2323 _LOTSNESTEDMESSAGE_B213._serialized_start=2325 _LOTSNESTEDMESSAGE_B213._serialized_end=2331 _LOTSNESTEDMESSAGE_B214._serialized_start=2333 _LOTSNESTEDMESSAGE_B214._serialized_end=2339 _LOTSNESTEDMESSAGE_B215._serialized_start=2341 _LOTSNESTEDMESSAGE_B215._serialized_end=2347 _LOTSNESTEDMESSAGE_B216._serialized_start=2349 _LOTSNESTEDMESSAGE_B216._serialized_end=2355 _LOTSNESTEDMESSAGE_B217._serialized_start=2357 _LOTSNESTEDMESSAGE_B217._serialized_end=2363 _LOTSNESTEDMESSAGE_B218._serialized_start=2365 _LOTSNESTEDMESSAGE_B218._serialized_end=2371 _LOTSNESTEDMESSAGE_B219._serialized_start=2373 _LOTSNESTEDMESSAGE_B219._serialized_end=2379 _LOTSNESTEDMESSAGE_B220._serialized_start=2381 _LOTSNESTEDMESSAGE_B220._serialized_end=2387 _LOTSNESTEDMESSAGE_B221._serialized_start=2389 _LOTSNESTEDMESSAGE_B221._serialized_end=2395 _LOTSNESTEDMESSAGE_B222._serialized_start=2397 _LOTSNESTEDMESSAGE_B222._serialized_end=2403 _LOTSNESTEDMESSAGE_B223._serialized_start=2405 _LOTSNESTEDMESSAGE_B223._serialized_end=2411 _LOTSNESTEDMESSAGE_B224._serialized_start=2413 _LOTSNESTEDMESSAGE_B224._serialized_end=2419 _LOTSNESTEDMESSAGE_B225._serialized_start=2421 _LOTSNESTEDMESSAGE_B225._serialized_end=2427 _LOTSNESTEDMESSAGE_B226._serialized_start=2429 _LOTSNESTEDMESSAGE_B226._serialized_end=2435 _LOTSNESTEDMESSAGE_B227._serialized_start=2437 _LOTSNESTEDMESSAGE_B227._serialized_end=2443 _LOTSNESTEDMESSAGE_B228._serialized_start=2445 _LOTSNESTEDMESSAGE_B228._serialized_end=2451 _LOTSNESTEDMESSAGE_B229._serialized_start=2453 _LOTSNESTEDMESSAGE_B229._serialized_end=2459 _LOTSNESTEDMESSAGE_B230._serialized_start=2461 _LOTSNESTEDMESSAGE_B230._serialized_end=2467 _LOTSNESTEDMESSAGE_B231._serialized_start=2469 _LOTSNESTEDMESSAGE_B231._serialized_end=2475 _LOTSNESTEDMESSAGE_B232._serialized_start=2477 _LOTSNESTEDMESSAGE_B232._serialized_end=2483 _LOTSNESTEDMESSAGE_B233._serialized_start=2485 _LOTSNESTEDMESSAGE_B233._serialized_end=2491 _LOTSNESTEDMESSAGE_B234._serialized_start=2493 _LOTSNESTEDMESSAGE_B234._serialized_end=2499 _LOTSNESTEDMESSAGE_B235._serialized_start=2501 _LOTSNESTEDMESSAGE_B235._serialized_end=2507 _LOTSNESTEDMESSAGE_B236._serialized_start=2509 _LOTSNESTEDMESSAGE_B236._serialized_end=2515 _LOTSNESTEDMESSAGE_B237._serialized_start=2517 _LOTSNESTEDMESSAGE_B237._serialized_end=2523 _LOTSNESTEDMESSAGE_B238._serialized_start=2525 _LOTSNESTEDMESSAGE_B238._serialized_end=2531 _LOTSNESTEDMESSAGE_B239._serialized_start=2533 _LOTSNESTEDMESSAGE_B239._serialized_end=2539 _LOTSNESTEDMESSAGE_B240._serialized_start=2541 _LOTSNESTEDMESSAGE_B240._serialized_end=2547 _LOTSNESTEDMESSAGE_B241._serialized_start=2549 _LOTSNESTEDMESSAGE_B241._serialized_end=2555 _LOTSNESTEDMESSAGE_B242._serialized_start=2557 _LOTSNESTEDMESSAGE_B242._serialized_end=2563 _LOTSNESTEDMESSAGE_B243._serialized_start=2565 _LOTSNESTEDMESSAGE_B243._serialized_end=2571 _LOTSNESTEDMESSAGE_B244._serialized_start=2573 _LOTSNESTEDMESSAGE_B244._serialized_end=2579 _LOTSNESTEDMESSAGE_B245._serialized_start=2581 _LOTSNESTEDMESSAGE_B245._serialized_end=2587 _LOTSNESTEDMESSAGE_B246._serialized_start=2589 _LOTSNESTEDMESSAGE_B246._serialized_end=2595 _LOTSNESTEDMESSAGE_B247._serialized_start=2597 _LOTSNESTEDMESSAGE_B247._serialized_end=2603 _LOTSNESTEDMESSAGE_B248._serialized_start=2605 _LOTSNESTEDMESSAGE_B248._serialized_end=2611 _LOTSNESTEDMESSAGE_B249._serialized_start=2613 _LOTSNESTEDMESSAGE_B249._serialized_end=2619 _LOTSNESTEDMESSAGE_B250._serialized_start=2621 _LOTSNESTEDMESSAGE_B250._serialized_end=2627 _LOTSNESTEDMESSAGE_B251._serialized_start=2629 _LOTSNESTEDMESSAGE_B251._serialized_end=2635 _LOTSNESTEDMESSAGE_B252._serialized_start=2637 _LOTSNESTEDMESSAGE_B252._serialized_end=2643 _LOTSNESTEDMESSAGE_B253._serialized_start=2645 _LOTSNESTEDMESSAGE_B253._serialized_end=2651 _LOTSNESTEDMESSAGE_B254._serialized_start=2653 _LOTSNESTEDMESSAGE_B254._serialized_end=2659 _LOTSNESTEDMESSAGE_B255._serialized_start=2661 _LOTSNESTEDMESSAGE_B255._serialized_end=2667 # @@protoc_insertion_point(module_scope) ================================================ FILE: openpype/hosts/nuke/vendor/google/protobuf/internal/no_package_pb2.py ================================================ # -*- coding: utf-8 -*- # Generated by the protocol buffer compiler. DO NOT EDIT! # source: google/protobuf/internal/no_package.proto """Generated protocol buffer code.""" from google.protobuf.internal import builder as _builder from google.protobuf import descriptor as _descriptor from google.protobuf import descriptor_pool as _descriptor_pool from google.protobuf import symbol_database as _symbol_database # @@protoc_insertion_point(imports) _sym_db = _symbol_database.Default() DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n)google/protobuf/internal/no_package.proto\";\n\x10NoPackageMessage\x12\'\n\x0fno_package_enum\x18\x01 \x01(\x0e\x32\x0e.NoPackageEnum*?\n\rNoPackageEnum\x12\x16\n\x12NO_PACKAGE_VALUE_0\x10\x00\x12\x16\n\x12NO_PACKAGE_VALUE_1\x10\x01') _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals()) _builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'google.protobuf.internal.no_package_pb2', globals()) if _descriptor._USE_C_DESCRIPTORS == False: DESCRIPTOR._options = None _NOPACKAGEENUM._serialized_start=106 _NOPACKAGEENUM._serialized_end=169 _NOPACKAGEMESSAGE._serialized_start=45 _NOPACKAGEMESSAGE._serialized_end=104 # @@protoc_insertion_point(module_scope) ================================================ FILE: openpype/hosts/nuke/vendor/google/protobuf/internal/python_message.py ================================================ # Protocol Buffers - Google's data interchange format # Copyright 2008 Google Inc. All rights reserved. # https://developers.google.com/protocol-buffers/ # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are # met: # # * Redistributions of source code must retain the above copyright # notice, this list of conditions and the following disclaimer. # * Redistributions in binary form must reproduce the above # copyright notice, this list of conditions and the following disclaimer # in the documentation and/or other materials provided with the # distribution. # * Neither the name of Google Inc. nor the names of its # contributors may be used to endorse or promote products derived from # this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. # This code is meant to work on Python 2.4 and above only. # # TODO(robinson): Helpers for verbose, common checks like seeing if a # descriptor's cpp_type is CPPTYPE_MESSAGE. """Contains a metaclass and helper functions used to create protocol message classes from Descriptor objects at runtime. Recall that a metaclass is the "type" of a class. (A class is to a metaclass what an instance is to a class.) In this case, we use the GeneratedProtocolMessageType metaclass to inject all the useful functionality into the classes output by the protocol compiler at compile-time. The upshot of all this is that the real implementation details for ALL pure-Python protocol buffers are *here in this file*. """ __author__ = 'robinson@google.com (Will Robinson)' from io import BytesIO import struct import sys import weakref # We use "as" to avoid name collisions with variables. from google.protobuf.internal import api_implementation from google.protobuf.internal import containers from google.protobuf.internal import decoder from google.protobuf.internal import encoder from google.protobuf.internal import enum_type_wrapper from google.protobuf.internal import extension_dict from google.protobuf.internal import message_listener as message_listener_mod from google.protobuf.internal import type_checkers from google.protobuf.internal import well_known_types from google.protobuf.internal import wire_format from google.protobuf import descriptor as descriptor_mod from google.protobuf import message as message_mod from google.protobuf import text_format _FieldDescriptor = descriptor_mod.FieldDescriptor _AnyFullTypeName = 'google.protobuf.Any' _ExtensionDict = extension_dict._ExtensionDict class GeneratedProtocolMessageType(type): """Metaclass for protocol message classes created at runtime from Descriptors. We add implementations for all methods described in the Message class. We also create properties to allow getting/setting all fields in the protocol message. Finally, we create slots to prevent users from accidentally "setting" nonexistent fields in the protocol message, which then wouldn't get serialized / deserialized properly. The protocol compiler currently uses this metaclass to create protocol message classes at runtime. Clients can also manually create their own classes at runtime, as in this example: mydescriptor = Descriptor(.....) factory = symbol_database.Default() factory.pool.AddDescriptor(mydescriptor) MyProtoClass = factory.GetPrototype(mydescriptor) myproto_instance = MyProtoClass() myproto.foo_field = 23 ... """ # Must be consistent with the protocol-compiler code in # proto2/compiler/internal/generator.*. _DESCRIPTOR_KEY = 'DESCRIPTOR' def __new__(cls, name, bases, dictionary): """Custom allocation for runtime-generated class types. We override __new__ because this is apparently the only place where we can meaningfully set __slots__ on the class we're creating(?). (The interplay between metaclasses and slots is not very well-documented). Args: name: Name of the class (ignored, but required by the metaclass protocol). bases: Base classes of the class we're constructing. (Should be message.Message). We ignore this field, but it's required by the metaclass protocol dictionary: The class dictionary of the class we're constructing. dictionary[_DESCRIPTOR_KEY] must contain a Descriptor object describing this protocol message type. Returns: Newly-allocated class. Raises: RuntimeError: Generated code only work with python cpp extension. """ descriptor = dictionary[GeneratedProtocolMessageType._DESCRIPTOR_KEY] if isinstance(descriptor, str): raise RuntimeError('The generated code only work with python cpp ' 'extension, but it is using pure python runtime.') # If a concrete class already exists for this descriptor, don't try to # create another. Doing so will break any messages that already exist with # the existing class. # # The C++ implementation appears to have its own internal `PyMessageFactory` # to achieve similar results. # # This most commonly happens in `text_format.py` when using descriptors from # a custom pool; it calls symbol_database.Global().getPrototype() on a # descriptor which already has an existing concrete class. new_class = getattr(descriptor, '_concrete_class', None) if new_class: return new_class if descriptor.full_name in well_known_types.WKTBASES: bases += (well_known_types.WKTBASES[descriptor.full_name],) _AddClassAttributesForNestedExtensions(descriptor, dictionary) _AddSlots(descriptor, dictionary) superclass = super(GeneratedProtocolMessageType, cls) new_class = superclass.__new__(cls, name, bases, dictionary) return new_class def __init__(cls, name, bases, dictionary): """Here we perform the majority of our work on the class. We add enum getters, an __init__ method, implementations of all Message methods, and properties for all fields in the protocol type. Args: name: Name of the class (ignored, but required by the metaclass protocol). bases: Base classes of the class we're constructing. (Should be message.Message). We ignore this field, but it's required by the metaclass protocol dictionary: The class dictionary of the class we're constructing. dictionary[_DESCRIPTOR_KEY] must contain a Descriptor object describing this protocol message type. """ descriptor = dictionary[GeneratedProtocolMessageType._DESCRIPTOR_KEY] # If this is an _existing_ class looked up via `_concrete_class` in the # __new__ method above, then we don't need to re-initialize anything. existing_class = getattr(descriptor, '_concrete_class', None) if existing_class: assert existing_class is cls, ( 'Duplicate `GeneratedProtocolMessageType` created for descriptor %r' % (descriptor.full_name)) return cls._decoders_by_tag = {} if (descriptor.has_options and descriptor.GetOptions().message_set_wire_format): cls._decoders_by_tag[decoder.MESSAGE_SET_ITEM_TAG] = ( decoder.MessageSetItemDecoder(descriptor), None) # Attach stuff to each FieldDescriptor for quick lookup later on. for field in descriptor.fields: _AttachFieldHelpers(cls, field) descriptor._concrete_class = cls # pylint: disable=protected-access _AddEnumValues(descriptor, cls) _AddInitMethod(descriptor, cls) _AddPropertiesForFields(descriptor, cls) _AddPropertiesForExtensions(descriptor, cls) _AddStaticMethods(cls) _AddMessageMethods(descriptor, cls) _AddPrivateHelperMethods(descriptor, cls) superclass = super(GeneratedProtocolMessageType, cls) superclass.__init__(name, bases, dictionary) # Stateless helpers for GeneratedProtocolMessageType below. # Outside clients should not access these directly. # # I opted not to make any of these methods on the metaclass, to make it more # clear that I'm not really using any state there and to keep clients from # thinking that they have direct access to these construction helpers. def _PropertyName(proto_field_name): """Returns the name of the public property attribute which clients can use to get and (in some cases) set the value of a protocol message field. Args: proto_field_name: The protocol message field name, exactly as it appears (or would appear) in a .proto file. """ # TODO(robinson): Escape Python keywords (e.g., yield), and test this support. # nnorwitz makes my day by writing: # """ # FYI. See the keyword module in the stdlib. This could be as simple as: # # if keyword.iskeyword(proto_field_name): # return proto_field_name + "_" # return proto_field_name # """ # Kenton says: The above is a BAD IDEA. People rely on being able to use # getattr() and setattr() to reflectively manipulate field values. If we # rename the properties, then every such user has to also make sure to apply # the same transformation. Note that currently if you name a field "yield", # you can still access it just fine using getattr/setattr -- it's not even # that cumbersome to do so. # TODO(kenton): Remove this method entirely if/when everyone agrees with my # position. return proto_field_name def _AddSlots(message_descriptor, dictionary): """Adds a __slots__ entry to dictionary, containing the names of all valid attributes for this message type. Args: message_descriptor: A Descriptor instance describing this message type. dictionary: Class dictionary to which we'll add a '__slots__' entry. """ dictionary['__slots__'] = ['_cached_byte_size', '_cached_byte_size_dirty', '_fields', '_unknown_fields', '_unknown_field_set', '_is_present_in_parent', '_listener', '_listener_for_children', '__weakref__', '_oneofs'] def _IsMessageSetExtension(field): return (field.is_extension and field.containing_type.has_options and field.containing_type.GetOptions().message_set_wire_format and field.type == _FieldDescriptor.TYPE_MESSAGE and field.label == _FieldDescriptor.LABEL_OPTIONAL) def _IsMapField(field): return (field.type == _FieldDescriptor.TYPE_MESSAGE and field.message_type.has_options and field.message_type.GetOptions().map_entry) def _IsMessageMapField(field): value_type = field.message_type.fields_by_name['value'] return value_type.cpp_type == _FieldDescriptor.CPPTYPE_MESSAGE def _AttachFieldHelpers(cls, field_descriptor): is_repeated = (field_descriptor.label == _FieldDescriptor.LABEL_REPEATED) is_packable = (is_repeated and wire_format.IsTypePackable(field_descriptor.type)) is_proto3 = field_descriptor.containing_type.syntax == 'proto3' if not is_packable: is_packed = False elif field_descriptor.containing_type.syntax == 'proto2': is_packed = (field_descriptor.has_options and field_descriptor.GetOptions().packed) else: has_packed_false = (field_descriptor.has_options and field_descriptor.GetOptions().HasField('packed') and field_descriptor.GetOptions().packed == False) is_packed = not has_packed_false is_map_entry = _IsMapField(field_descriptor) if is_map_entry: field_encoder = encoder.MapEncoder(field_descriptor) sizer = encoder.MapSizer(field_descriptor, _IsMessageMapField(field_descriptor)) elif _IsMessageSetExtension(field_descriptor): field_encoder = encoder.MessageSetItemEncoder(field_descriptor.number) sizer = encoder.MessageSetItemSizer(field_descriptor.number) else: field_encoder = type_checkers.TYPE_TO_ENCODER[field_descriptor.type]( field_descriptor.number, is_repeated, is_packed) sizer = type_checkers.TYPE_TO_SIZER[field_descriptor.type]( field_descriptor.number, is_repeated, is_packed) field_descriptor._encoder = field_encoder field_descriptor._sizer = sizer field_descriptor._default_constructor = _DefaultValueConstructorForField( field_descriptor) def AddDecoder(wiretype, is_packed): tag_bytes = encoder.TagBytes(field_descriptor.number, wiretype) decode_type = field_descriptor.type if (decode_type == _FieldDescriptor.TYPE_ENUM and type_checkers.SupportsOpenEnums(field_descriptor)): decode_type = _FieldDescriptor.TYPE_INT32 oneof_descriptor = None clear_if_default = False if field_descriptor.containing_oneof is not None: oneof_descriptor = field_descriptor elif (is_proto3 and not is_repeated and field_descriptor.cpp_type != _FieldDescriptor.CPPTYPE_MESSAGE): clear_if_default = True if is_map_entry: is_message_map = _IsMessageMapField(field_descriptor) field_decoder = decoder.MapDecoder( field_descriptor, _GetInitializeDefaultForMap(field_descriptor), is_message_map) elif decode_type == _FieldDescriptor.TYPE_STRING: field_decoder = decoder.StringDecoder( field_descriptor.number, is_repeated, is_packed, field_descriptor, field_descriptor._default_constructor, clear_if_default) elif field_descriptor.cpp_type == _FieldDescriptor.CPPTYPE_MESSAGE: field_decoder = type_checkers.TYPE_TO_DECODER[decode_type]( field_descriptor.number, is_repeated, is_packed, field_descriptor, field_descriptor._default_constructor) else: field_decoder = type_checkers.TYPE_TO_DECODER[decode_type]( field_descriptor.number, is_repeated, is_packed, # pylint: disable=protected-access field_descriptor, field_descriptor._default_constructor, clear_if_default) cls._decoders_by_tag[tag_bytes] = (field_decoder, oneof_descriptor) AddDecoder(type_checkers.FIELD_TYPE_TO_WIRE_TYPE[field_descriptor.type], False) if is_repeated and wire_format.IsTypePackable(field_descriptor.type): # To support wire compatibility of adding packed = true, add a decoder for # packed values regardless of the field's options. AddDecoder(wire_format.WIRETYPE_LENGTH_DELIMITED, True) def _AddClassAttributesForNestedExtensions(descriptor, dictionary): extensions = descriptor.extensions_by_name for extension_name, extension_field in extensions.items(): assert extension_name not in dictionary dictionary[extension_name] = extension_field def _AddEnumValues(descriptor, cls): """Sets class-level attributes for all enum fields defined in this message. Also exporting a class-level object that can name enum values. Args: descriptor: Descriptor object for this message type. cls: Class we're constructing for this message type. """ for enum_type in descriptor.enum_types: setattr(cls, enum_type.name, enum_type_wrapper.EnumTypeWrapper(enum_type)) for enum_value in enum_type.values: setattr(cls, enum_value.name, enum_value.number) def _GetInitializeDefaultForMap(field): if field.label != _FieldDescriptor.LABEL_REPEATED: raise ValueError('map_entry set on non-repeated field %s' % ( field.name)) fields_by_name = field.message_type.fields_by_name key_checker = type_checkers.GetTypeChecker(fields_by_name['key']) value_field = fields_by_name['value'] if _IsMessageMapField(field): def MakeMessageMapDefault(message): return containers.MessageMap( message._listener_for_children, value_field.message_type, key_checker, field.message_type) return MakeMessageMapDefault else: value_checker = type_checkers.GetTypeChecker(value_field) def MakePrimitiveMapDefault(message): return containers.ScalarMap( message._listener_for_children, key_checker, value_checker, field.message_type) return MakePrimitiveMapDefault def _DefaultValueConstructorForField(field): """Returns a function which returns a default value for a field. Args: field: FieldDescriptor object for this field. The returned function has one argument: message: Message instance containing this field, or a weakref proxy of same. That function in turn returns a default value for this field. The default value may refer back to |message| via a weak reference. """ if _IsMapField(field): return _GetInitializeDefaultForMap(field) if field.label == _FieldDescriptor.LABEL_REPEATED: if field.has_default_value and field.default_value != []: raise ValueError('Repeated field default value not empty list: %s' % ( field.default_value)) if field.cpp_type == _FieldDescriptor.CPPTYPE_MESSAGE: # We can't look at _concrete_class yet since it might not have # been set. (Depends on order in which we initialize the classes). message_type = field.message_type def MakeRepeatedMessageDefault(message): return containers.RepeatedCompositeFieldContainer( message._listener_for_children, field.message_type) return MakeRepeatedMessageDefault else: type_checker = type_checkers.GetTypeChecker(field) def MakeRepeatedScalarDefault(message): return containers.RepeatedScalarFieldContainer( message._listener_for_children, type_checker) return MakeRepeatedScalarDefault if field.cpp_type == _FieldDescriptor.CPPTYPE_MESSAGE: # _concrete_class may not yet be initialized. message_type = field.message_type def MakeSubMessageDefault(message): assert getattr(message_type, '_concrete_class', None), ( 'Uninitialized concrete class found for field %r (message type %r)' % (field.full_name, message_type.full_name)) result = message_type._concrete_class() result._SetListener( _OneofListener(message, field) if field.containing_oneof is not None else message._listener_for_children) return result return MakeSubMessageDefault def MakeScalarDefault(message): # TODO(protobuf-team): This may be broken since there may not be # default_value. Combine with has_default_value somehow. return field.default_value return MakeScalarDefault def _ReraiseTypeErrorWithFieldName(message_name, field_name): """Re-raise the currently-handled TypeError with the field name added.""" exc = sys.exc_info()[1] if len(exc.args) == 1 and type(exc) is TypeError: # simple TypeError; add field name to exception message exc = TypeError('%s for field %s.%s' % (str(exc), message_name, field_name)) # re-raise possibly-amended exception with original traceback: raise exc.with_traceback(sys.exc_info()[2]) def _AddInitMethod(message_descriptor, cls): """Adds an __init__ method to cls.""" def _GetIntegerEnumValue(enum_type, value): """Convert a string or integer enum value to an integer. If the value is a string, it is converted to the enum value in enum_type with the same name. If the value is not a string, it's returned as-is. (No conversion or bounds-checking is done.) """ if isinstance(value, str): try: return enum_type.values_by_name[value].number except KeyError: raise ValueError('Enum type %s: unknown label "%s"' % ( enum_type.full_name, value)) return value def init(self, **kwargs): self._cached_byte_size = 0 self._cached_byte_size_dirty = len(kwargs) > 0 self._fields = {} # Contains a mapping from oneof field descriptors to the descriptor # of the currently set field in that oneof field. self._oneofs = {} # _unknown_fields is () when empty for efficiency, and will be turned into # a list if fields are added. self._unknown_fields = () # _unknown_field_set is None when empty for efficiency, and will be # turned into UnknownFieldSet struct if fields are added. self._unknown_field_set = None # pylint: disable=protected-access self._is_present_in_parent = False self._listener = message_listener_mod.NullMessageListener() self._listener_for_children = _Listener(self) for field_name, field_value in kwargs.items(): field = _GetFieldByName(message_descriptor, field_name) if field is None: raise TypeError('%s() got an unexpected keyword argument "%s"' % (message_descriptor.name, field_name)) if field_value is None: # field=None is the same as no field at all. continue if field.label == _FieldDescriptor.LABEL_REPEATED: copy = field._default_constructor(self) if field.cpp_type == _FieldDescriptor.CPPTYPE_MESSAGE: # Composite if _IsMapField(field): if _IsMessageMapField(field): for key in field_value: copy[key].MergeFrom(field_value[key]) else: copy.update(field_value) else: for val in field_value: if isinstance(val, dict): copy.add(**val) else: copy.add().MergeFrom(val) else: # Scalar if field.cpp_type == _FieldDescriptor.CPPTYPE_ENUM: field_value = [_GetIntegerEnumValue(field.enum_type, val) for val in field_value] copy.extend(field_value) self._fields[field] = copy elif field.cpp_type == _FieldDescriptor.CPPTYPE_MESSAGE: copy = field._default_constructor(self) new_val = field_value if isinstance(field_value, dict): new_val = field.message_type._concrete_class(**field_value) try: copy.MergeFrom(new_val) except TypeError: _ReraiseTypeErrorWithFieldName(message_descriptor.name, field_name) self._fields[field] = copy else: if field.cpp_type == _FieldDescriptor.CPPTYPE_ENUM: field_value = _GetIntegerEnumValue(field.enum_type, field_value) try: setattr(self, field_name, field_value) except TypeError: _ReraiseTypeErrorWithFieldName(message_descriptor.name, field_name) init.__module__ = None init.__doc__ = None cls.__init__ = init def _GetFieldByName(message_descriptor, field_name): """Returns a field descriptor by field name. Args: message_descriptor: A Descriptor describing all fields in message. field_name: The name of the field to retrieve. Returns: The field descriptor associated with the field name. """ try: return message_descriptor.fields_by_name[field_name] except KeyError: raise ValueError('Protocol message %s has no "%s" field.' % (message_descriptor.name, field_name)) def _AddPropertiesForFields(descriptor, cls): """Adds properties for all fields in this protocol message type.""" for field in descriptor.fields: _AddPropertiesForField(field, cls) if descriptor.is_extendable: # _ExtensionDict is just an adaptor with no state so we allocate a new one # every time it is accessed. cls.Extensions = property(lambda self: _ExtensionDict(self)) def _AddPropertiesForField(field, cls): """Adds a public property for a protocol message field. Clients can use this property to get and (in the case of non-repeated scalar fields) directly set the value of a protocol message field. Args: field: A FieldDescriptor for this field. cls: The class we're constructing. """ # Catch it if we add other types that we should # handle specially here. assert _FieldDescriptor.MAX_CPPTYPE == 10 constant_name = field.name.upper() + '_FIELD_NUMBER' setattr(cls, constant_name, field.number) if field.label == _FieldDescriptor.LABEL_REPEATED: _AddPropertiesForRepeatedField(field, cls) elif field.cpp_type == _FieldDescriptor.CPPTYPE_MESSAGE: _AddPropertiesForNonRepeatedCompositeField(field, cls) else: _AddPropertiesForNonRepeatedScalarField(field, cls) class _FieldProperty(property): __slots__ = ('DESCRIPTOR',) def __init__(self, descriptor, getter, setter, doc): property.__init__(self, getter, setter, doc=doc) self.DESCRIPTOR = descriptor def _AddPropertiesForRepeatedField(field, cls): """Adds a public property for a "repeated" protocol message field. Clients can use this property to get the value of the field, which will be either a RepeatedScalarFieldContainer or RepeatedCompositeFieldContainer (see below). Note that when clients add values to these containers, we perform type-checking in the case of repeated scalar fields, and we also set any necessary "has" bits as a side-effect. Args: field: A FieldDescriptor for this field. cls: The class we're constructing. """ proto_field_name = field.name property_name = _PropertyName(proto_field_name) def getter(self): field_value = self._fields.get(field) if field_value is None: # Construct a new object to represent this field. field_value = field._default_constructor(self) # Atomically check if another thread has preempted us and, if not, swap # in the new object we just created. If someone has preempted us, we # take that object and discard ours. # WARNING: We are relying on setdefault() being atomic. This is true # in CPython but we haven't investigated others. This warning appears # in several other locations in this file. field_value = self._fields.setdefault(field, field_value) return field_value getter.__module__ = None getter.__doc__ = 'Getter for %s.' % proto_field_name # We define a setter just so we can throw an exception with a more # helpful error message. def setter(self, new_value): raise AttributeError('Assignment not allowed to repeated field ' '"%s" in protocol message object.' % proto_field_name) doc = 'Magic attribute generated for "%s" proto field.' % proto_field_name setattr(cls, property_name, _FieldProperty(field, getter, setter, doc=doc)) def _AddPropertiesForNonRepeatedScalarField(field, cls): """Adds a public property for a nonrepeated, scalar protocol message field. Clients can use this property to get and directly set the value of the field. Note that when the client sets the value of a field by using this property, all necessary "has" bits are set as a side-effect, and we also perform type-checking. Args: field: A FieldDescriptor for this field. cls: The class we're constructing. """ proto_field_name = field.name property_name = _PropertyName(proto_field_name) type_checker = type_checkers.GetTypeChecker(field) default_value = field.default_value is_proto3 = field.containing_type.syntax == 'proto3' def getter(self): # TODO(protobuf-team): This may be broken since there may not be # default_value. Combine with has_default_value somehow. return self._fields.get(field, default_value) getter.__module__ = None getter.__doc__ = 'Getter for %s.' % proto_field_name clear_when_set_to_default = is_proto3 and not field.containing_oneof def field_setter(self, new_value): # pylint: disable=protected-access # Testing the value for truthiness captures all of the proto3 defaults # (0, 0.0, enum 0, and False). try: new_value = type_checker.CheckValue(new_value) except TypeError as e: raise TypeError( 'Cannot set %s to %.1024r: %s' % (field.full_name, new_value, e)) if clear_when_set_to_default and not new_value: self._fields.pop(field, None) else: self._fields[field] = new_value # Check _cached_byte_size_dirty inline to improve performance, since scalar # setters are called frequently. if not self._cached_byte_size_dirty: self._Modified() if field.containing_oneof: def setter(self, new_value): field_setter(self, new_value) self._UpdateOneofState(field) else: setter = field_setter setter.__module__ = None setter.__doc__ = 'Setter for %s.' % proto_field_name # Add a property to encapsulate the getter/setter. doc = 'Magic attribute generated for "%s" proto field.' % proto_field_name setattr(cls, property_name, _FieldProperty(field, getter, setter, doc=doc)) def _AddPropertiesForNonRepeatedCompositeField(field, cls): """Adds a public property for a nonrepeated, composite protocol message field. A composite field is a "group" or "message" field. Clients can use this property to get the value of the field, but cannot assign to the property directly. Args: field: A FieldDescriptor for this field. cls: The class we're constructing. """ # TODO(robinson): Remove duplication with similar method # for non-repeated scalars. proto_field_name = field.name property_name = _PropertyName(proto_field_name) def getter(self): field_value = self._fields.get(field) if field_value is None: # Construct a new object to represent this field. field_value = field._default_constructor(self) # Atomically check if another thread has preempted us and, if not, swap # in the new object we just created. If someone has preempted us, we # take that object and discard ours. # WARNING: We are relying on setdefault() being atomic. This is true # in CPython but we haven't investigated others. This warning appears # in several other locations in this file. field_value = self._fields.setdefault(field, field_value) return field_value getter.__module__ = None getter.__doc__ = 'Getter for %s.' % proto_field_name # We define a setter just so we can throw an exception with a more # helpful error message. def setter(self, new_value): raise AttributeError('Assignment not allowed to composite field ' '"%s" in protocol message object.' % proto_field_name) # Add a property to encapsulate the getter. doc = 'Magic attribute generated for "%s" proto field.' % proto_field_name setattr(cls, property_name, _FieldProperty(field, getter, setter, doc=doc)) def _AddPropertiesForExtensions(descriptor, cls): """Adds properties for all fields in this protocol message type.""" extensions = descriptor.extensions_by_name for extension_name, extension_field in extensions.items(): constant_name = extension_name.upper() + '_FIELD_NUMBER' setattr(cls, constant_name, extension_field.number) # TODO(amauryfa): Migrate all users of these attributes to functions like # pool.FindExtensionByNumber(descriptor). if descriptor.file is not None: # TODO(amauryfa): Use cls.MESSAGE_FACTORY.pool when available. pool = descriptor.file.pool cls._extensions_by_number = pool._extensions_by_number[descriptor] cls._extensions_by_name = pool._extensions_by_name[descriptor] def _AddStaticMethods(cls): # TODO(robinson): This probably needs to be thread-safe(?) def RegisterExtension(extension_handle): extension_handle.containing_type = cls.DESCRIPTOR # TODO(amauryfa): Use cls.MESSAGE_FACTORY.pool when available. # pylint: disable=protected-access cls.DESCRIPTOR.file.pool._AddExtensionDescriptor(extension_handle) _AttachFieldHelpers(cls, extension_handle) cls.RegisterExtension = staticmethod(RegisterExtension) def FromString(s): message = cls() message.MergeFromString(s) return message cls.FromString = staticmethod(FromString) def _IsPresent(item): """Given a (FieldDescriptor, value) tuple from _fields, return true if the value should be included in the list returned by ListFields().""" if item[0].label == _FieldDescriptor.LABEL_REPEATED: return bool(item[1]) elif item[0].cpp_type == _FieldDescriptor.CPPTYPE_MESSAGE: return item[1]._is_present_in_parent else: return True def _AddListFieldsMethod(message_descriptor, cls): """Helper for _AddMessageMethods().""" def ListFields(self): all_fields = [item for item in self._fields.items() if _IsPresent(item)] all_fields.sort(key = lambda item: item[0].number) return all_fields cls.ListFields = ListFields _PROTO3_ERROR_TEMPLATE = \ ('Protocol message %s has no non-repeated submessage field "%s" ' 'nor marked as optional') _PROTO2_ERROR_TEMPLATE = 'Protocol message %s has no non-repeated field "%s"' def _AddHasFieldMethod(message_descriptor, cls): """Helper for _AddMessageMethods().""" is_proto3 = (message_descriptor.syntax == "proto3") error_msg = _PROTO3_ERROR_TEMPLATE if is_proto3 else _PROTO2_ERROR_TEMPLATE hassable_fields = {} for field in message_descriptor.fields: if field.label == _FieldDescriptor.LABEL_REPEATED: continue # For proto3, only submessages and fields inside a oneof have presence. if (is_proto3 and field.cpp_type != _FieldDescriptor.CPPTYPE_MESSAGE and not field.containing_oneof): continue hassable_fields[field.name] = field # Has methods are supported for oneof descriptors. for oneof in message_descriptor.oneofs: hassable_fields[oneof.name] = oneof def HasField(self, field_name): try: field = hassable_fields[field_name] except KeyError: raise ValueError(error_msg % (message_descriptor.full_name, field_name)) if isinstance(field, descriptor_mod.OneofDescriptor): try: return HasField(self, self._oneofs[field].name) except KeyError: return False else: if field.cpp_type == _FieldDescriptor.CPPTYPE_MESSAGE: value = self._fields.get(field) return value is not None and value._is_present_in_parent else: return field in self._fields cls.HasField = HasField def _AddClearFieldMethod(message_descriptor, cls): """Helper for _AddMessageMethods().""" def ClearField(self, field_name): try: field = message_descriptor.fields_by_name[field_name] except KeyError: try: field = message_descriptor.oneofs_by_name[field_name] if field in self._oneofs: field = self._oneofs[field] else: return except KeyError: raise ValueError('Protocol message %s has no "%s" field.' % (message_descriptor.name, field_name)) if field in self._fields: # To match the C++ implementation, we need to invalidate iterators # for map fields when ClearField() happens. if hasattr(self._fields[field], 'InvalidateIterators'): self._fields[field].InvalidateIterators() # Note: If the field is a sub-message, its listener will still point # at us. That's fine, because the worst than can happen is that it # will call _Modified() and invalidate our byte size. Big deal. del self._fields[field] if self._oneofs.get(field.containing_oneof, None) is field: del self._oneofs[field.containing_oneof] # Always call _Modified() -- even if nothing was changed, this is # a mutating method, and thus calling it should cause the field to become # present in the parent message. self._Modified() cls.ClearField = ClearField def _AddClearExtensionMethod(cls): """Helper for _AddMessageMethods().""" def ClearExtension(self, extension_handle): extension_dict._VerifyExtensionHandle(self, extension_handle) # Similar to ClearField(), above. if extension_handle in self._fields: del self._fields[extension_handle] self._Modified() cls.ClearExtension = ClearExtension def _AddHasExtensionMethod(cls): """Helper for _AddMessageMethods().""" def HasExtension(self, extension_handle): extension_dict._VerifyExtensionHandle(self, extension_handle) if extension_handle.label == _FieldDescriptor.LABEL_REPEATED: raise KeyError('"%s" is repeated.' % extension_handle.full_name) if extension_handle.cpp_type == _FieldDescriptor.CPPTYPE_MESSAGE: value = self._fields.get(extension_handle) return value is not None and value._is_present_in_parent else: return extension_handle in self._fields cls.HasExtension = HasExtension def _InternalUnpackAny(msg): """Unpacks Any message and returns the unpacked message. This internal method is different from public Any Unpack method which takes the target message as argument. _InternalUnpackAny method does not have target message type and need to find the message type in descriptor pool. Args: msg: An Any message to be unpacked. Returns: The unpacked message. """ # TODO(amauryfa): Don't use the factory of generated messages. # To make Any work with custom factories, use the message factory of the # parent message. # pylint: disable=g-import-not-at-top from google.protobuf import symbol_database factory = symbol_database.Default() type_url = msg.type_url if not type_url: return None # TODO(haberman): For now we just strip the hostname. Better logic will be # required. type_name = type_url.split('/')[-1] descriptor = factory.pool.FindMessageTypeByName(type_name) if descriptor is None: return None message_class = factory.GetPrototype(descriptor) message = message_class() message.ParseFromString(msg.value) return message def _AddEqualsMethod(message_descriptor, cls): """Helper for _AddMessageMethods().""" def __eq__(self, other): if (not isinstance(other, message_mod.Message) or other.DESCRIPTOR != self.DESCRIPTOR): return False if self is other: return True if self.DESCRIPTOR.full_name == _AnyFullTypeName: any_a = _InternalUnpackAny(self) any_b = _InternalUnpackAny(other) if any_a and any_b: return any_a == any_b if not self.ListFields() == other.ListFields(): return False # TODO(jieluo): Fix UnknownFieldSet to consider MessageSet extensions, # then use it for the comparison. unknown_fields = list(self._unknown_fields) unknown_fields.sort() other_unknown_fields = list(other._unknown_fields) other_unknown_fields.sort() return unknown_fields == other_unknown_fields cls.__eq__ = __eq__ def _AddStrMethod(message_descriptor, cls): """Helper for _AddMessageMethods().""" def __str__(self): return text_format.MessageToString(self) cls.__str__ = __str__ def _AddReprMethod(message_descriptor, cls): """Helper for _AddMessageMethods().""" def __repr__(self): return text_format.MessageToString(self) cls.__repr__ = __repr__ def _AddUnicodeMethod(unused_message_descriptor, cls): """Helper for _AddMessageMethods().""" def __unicode__(self): return text_format.MessageToString(self, as_utf8=True).decode('utf-8') cls.__unicode__ = __unicode__ def _BytesForNonRepeatedElement(value, field_number, field_type): """Returns the number of bytes needed to serialize a non-repeated element. The returned byte count includes space for tag information and any other additional space associated with serializing value. Args: value: Value we're serializing. field_number: Field number of this value. (Since the field number is stored as part of a varint-encoded tag, this has an impact on the total bytes required to serialize the value). field_type: The type of the field. One of the TYPE_* constants within FieldDescriptor. """ try: fn = type_checkers.TYPE_TO_BYTE_SIZE_FN[field_type] return fn(field_number, value) except KeyError: raise message_mod.EncodeError('Unrecognized field type: %d' % field_type) def _AddByteSizeMethod(message_descriptor, cls): """Helper for _AddMessageMethods().""" def ByteSize(self): if not self._cached_byte_size_dirty: return self._cached_byte_size size = 0 descriptor = self.DESCRIPTOR if descriptor.GetOptions().map_entry: # Fields of map entry should always be serialized. size = descriptor.fields_by_name['key']._sizer(self.key) size += descriptor.fields_by_name['value']._sizer(self.value) else: for field_descriptor, field_value in self.ListFields(): size += field_descriptor._sizer(field_value) for tag_bytes, value_bytes in self._unknown_fields: size += len(tag_bytes) + len(value_bytes) self._cached_byte_size = size self._cached_byte_size_dirty = False self._listener_for_children.dirty = False return size cls.ByteSize = ByteSize def _AddSerializeToStringMethod(message_descriptor, cls): """Helper for _AddMessageMethods().""" def SerializeToString(self, **kwargs): # Check if the message has all of its required fields set. if not self.IsInitialized(): raise message_mod.EncodeError( 'Message %s is missing required fields: %s' % ( self.DESCRIPTOR.full_name, ','.join(self.FindInitializationErrors()))) return self.SerializePartialToString(**kwargs) cls.SerializeToString = SerializeToString def _AddSerializePartialToStringMethod(message_descriptor, cls): """Helper for _AddMessageMethods().""" def SerializePartialToString(self, **kwargs): out = BytesIO() self._InternalSerialize(out.write, **kwargs) return out.getvalue() cls.SerializePartialToString = SerializePartialToString def InternalSerialize(self, write_bytes, deterministic=None): if deterministic is None: deterministic = ( api_implementation.IsPythonDefaultSerializationDeterministic()) else: deterministic = bool(deterministic) descriptor = self.DESCRIPTOR if descriptor.GetOptions().map_entry: # Fields of map entry should always be serialized. descriptor.fields_by_name['key']._encoder( write_bytes, self.key, deterministic) descriptor.fields_by_name['value']._encoder( write_bytes, self.value, deterministic) else: for field_descriptor, field_value in self.ListFields(): field_descriptor._encoder(write_bytes, field_value, deterministic) for tag_bytes, value_bytes in self._unknown_fields: write_bytes(tag_bytes) write_bytes(value_bytes) cls._InternalSerialize = InternalSerialize def _AddMergeFromStringMethod(message_descriptor, cls): """Helper for _AddMessageMethods().""" def MergeFromString(self, serialized): serialized = memoryview(serialized) length = len(serialized) try: if self._InternalParse(serialized, 0, length) != length: # The only reason _InternalParse would return early is if it # encountered an end-group tag. raise message_mod.DecodeError('Unexpected end-group tag.') except (IndexError, TypeError): # Now ord(buf[p:p+1]) == ord('') gets TypeError. raise message_mod.DecodeError('Truncated message.') except struct.error as e: raise message_mod.DecodeError(e) return length # Return this for legacy reasons. cls.MergeFromString = MergeFromString local_ReadTag = decoder.ReadTag local_SkipField = decoder.SkipField decoders_by_tag = cls._decoders_by_tag def InternalParse(self, buffer, pos, end): """Create a message from serialized bytes. Args: self: Message, instance of the proto message object. buffer: memoryview of the serialized data. pos: int, position to start in the serialized data. end: int, end position of the serialized data. Returns: Message object. """ # Guard against internal misuse, since this function is called internally # quite extensively, and its easy to accidentally pass bytes. assert isinstance(buffer, memoryview) self._Modified() field_dict = self._fields # pylint: disable=protected-access unknown_field_set = self._unknown_field_set while pos != end: (tag_bytes, new_pos) = local_ReadTag(buffer, pos) field_decoder, field_desc = decoders_by_tag.get(tag_bytes, (None, None)) if field_decoder is None: if not self._unknown_fields: # pylint: disable=protected-access self._unknown_fields = [] # pylint: disable=protected-access if unknown_field_set is None: # pylint: disable=protected-access self._unknown_field_set = containers.UnknownFieldSet() # pylint: disable=protected-access unknown_field_set = self._unknown_field_set # pylint: disable=protected-access (tag, _) = decoder._DecodeVarint(tag_bytes, 0) field_number, wire_type = wire_format.UnpackTag(tag) if field_number == 0: raise message_mod.DecodeError('Field number 0 is illegal.') # TODO(jieluo): remove old_pos. old_pos = new_pos (data, new_pos) = decoder._DecodeUnknownField( buffer, new_pos, wire_type) # pylint: disable=protected-access if new_pos == -1: return pos # pylint: disable=protected-access unknown_field_set._add(field_number, wire_type, data) # TODO(jieluo): remove _unknown_fields. new_pos = local_SkipField(buffer, old_pos, end, tag_bytes) if new_pos == -1: return pos self._unknown_fields.append( (tag_bytes, buffer[old_pos:new_pos].tobytes())) pos = new_pos else: pos = field_decoder(buffer, new_pos, end, self, field_dict) if field_desc: self._UpdateOneofState(field_desc) return pos cls._InternalParse = InternalParse def _AddIsInitializedMethod(message_descriptor, cls): """Adds the IsInitialized and FindInitializationError methods to the protocol message class.""" required_fields = [field for field in message_descriptor.fields if field.label == _FieldDescriptor.LABEL_REQUIRED] def IsInitialized(self, errors=None): """Checks if all required fields of a message are set. Args: errors: A list which, if provided, will be populated with the field paths of all missing required fields. Returns: True iff the specified message has all required fields set. """ # Performance is critical so we avoid HasField() and ListFields(). for field in required_fields: if (field not in self._fields or (field.cpp_type == _FieldDescriptor.CPPTYPE_MESSAGE and not self._fields[field]._is_present_in_parent)): if errors is not None: errors.extend(self.FindInitializationErrors()) return False for field, value in list(self._fields.items()): # dict can change size! if field.cpp_type == _FieldDescriptor.CPPTYPE_MESSAGE: if field.label == _FieldDescriptor.LABEL_REPEATED: if (field.message_type.has_options and field.message_type.GetOptions().map_entry): continue for element in value: if not element.IsInitialized(): if errors is not None: errors.extend(self.FindInitializationErrors()) return False elif value._is_present_in_parent and not value.IsInitialized(): if errors is not None: errors.extend(self.FindInitializationErrors()) return False return True cls.IsInitialized = IsInitialized def FindInitializationErrors(self): """Finds required fields which are not initialized. Returns: A list of strings. Each string is a path to an uninitialized field from the top-level message, e.g. "foo.bar[5].baz". """ errors = [] # simplify things for field in required_fields: if not self.HasField(field.name): errors.append(field.name) for field, value in self.ListFields(): if field.cpp_type == _FieldDescriptor.CPPTYPE_MESSAGE: if field.is_extension: name = '(%s)' % field.full_name else: name = field.name if _IsMapField(field): if _IsMessageMapField(field): for key in value: element = value[key] prefix = '%s[%s].' % (name, key) sub_errors = element.FindInitializationErrors() errors += [prefix + error for error in sub_errors] else: # ScalarMaps can't have any initialization errors. pass elif field.label == _FieldDescriptor.LABEL_REPEATED: for i in range(len(value)): element = value[i] prefix = '%s[%d].' % (name, i) sub_errors = element.FindInitializationErrors() errors += [prefix + error for error in sub_errors] else: prefix = name + '.' sub_errors = value.FindInitializationErrors() errors += [prefix + error for error in sub_errors] return errors cls.FindInitializationErrors = FindInitializationErrors def _FullyQualifiedClassName(klass): module = klass.__module__ name = getattr(klass, '__qualname__', klass.__name__) if module in (None, 'builtins', '__builtin__'): return name return module + '.' + name def _AddMergeFromMethod(cls): LABEL_REPEATED = _FieldDescriptor.LABEL_REPEATED CPPTYPE_MESSAGE = _FieldDescriptor.CPPTYPE_MESSAGE def MergeFrom(self, msg): if not isinstance(msg, cls): raise TypeError( 'Parameter to MergeFrom() must be instance of same class: ' 'expected %s got %s.' % (_FullyQualifiedClassName(cls), _FullyQualifiedClassName(msg.__class__))) assert msg is not self self._Modified() fields = self._fields for field, value in msg._fields.items(): if field.label == LABEL_REPEATED: field_value = fields.get(field) if field_value is None: # Construct a new object to represent this field. field_value = field._default_constructor(self) fields[field] = field_value field_value.MergeFrom(value) elif field.cpp_type == CPPTYPE_MESSAGE: if value._is_present_in_parent: field_value = fields.get(field) if field_value is None: # Construct a new object to represent this field. field_value = field._default_constructor(self) fields[field] = field_value field_value.MergeFrom(value) else: self._fields[field] = value if field.containing_oneof: self._UpdateOneofState(field) if msg._unknown_fields: if not self._unknown_fields: self._unknown_fields = [] self._unknown_fields.extend(msg._unknown_fields) # pylint: disable=protected-access if self._unknown_field_set is None: self._unknown_field_set = containers.UnknownFieldSet() self._unknown_field_set._extend(msg._unknown_field_set) cls.MergeFrom = MergeFrom def _AddWhichOneofMethod(message_descriptor, cls): def WhichOneof(self, oneof_name): """Returns the name of the currently set field inside a oneof, or None.""" try: field = message_descriptor.oneofs_by_name[oneof_name] except KeyError: raise ValueError( 'Protocol message has no oneof "%s" field.' % oneof_name) nested_field = self._oneofs.get(field, None) if nested_field is not None and self.HasField(nested_field.name): return nested_field.name else: return None cls.WhichOneof = WhichOneof def _Clear(self): # Clear fields. self._fields = {} self._unknown_fields = () # pylint: disable=protected-access if self._unknown_field_set is not None: self._unknown_field_set._clear() self._unknown_field_set = None self._oneofs = {} self._Modified() def _UnknownFields(self): if self._unknown_field_set is None: # pylint: disable=protected-access # pylint: disable=protected-access self._unknown_field_set = containers.UnknownFieldSet() return self._unknown_field_set # pylint: disable=protected-access def _DiscardUnknownFields(self): self._unknown_fields = [] self._unknown_field_set = None # pylint: disable=protected-access for field, value in self.ListFields(): if field.cpp_type == _FieldDescriptor.CPPTYPE_MESSAGE: if _IsMapField(field): if _IsMessageMapField(field): for key in value: value[key].DiscardUnknownFields() elif field.label == _FieldDescriptor.LABEL_REPEATED: for sub_message in value: sub_message.DiscardUnknownFields() else: value.DiscardUnknownFields() def _SetListener(self, listener): if listener is None: self._listener = message_listener_mod.NullMessageListener() else: self._listener = listener def _AddMessageMethods(message_descriptor, cls): """Adds implementations of all Message methods to cls.""" _AddListFieldsMethod(message_descriptor, cls) _AddHasFieldMethod(message_descriptor, cls) _AddClearFieldMethod(message_descriptor, cls) if message_descriptor.is_extendable: _AddClearExtensionMethod(cls) _AddHasExtensionMethod(cls) _AddEqualsMethod(message_descriptor, cls) _AddStrMethod(message_descriptor, cls) _AddReprMethod(message_descriptor, cls) _AddUnicodeMethod(message_descriptor, cls) _AddByteSizeMethod(message_descriptor, cls) _AddSerializeToStringMethod(message_descriptor, cls) _AddSerializePartialToStringMethod(message_descriptor, cls) _AddMergeFromStringMethod(message_descriptor, cls) _AddIsInitializedMethod(message_descriptor, cls) _AddMergeFromMethod(cls) _AddWhichOneofMethod(message_descriptor, cls) # Adds methods which do not depend on cls. cls.Clear = _Clear cls.UnknownFields = _UnknownFields cls.DiscardUnknownFields = _DiscardUnknownFields cls._SetListener = _SetListener def _AddPrivateHelperMethods(message_descriptor, cls): """Adds implementation of private helper methods to cls.""" def Modified(self): """Sets the _cached_byte_size_dirty bit to true, and propagates this to our listener iff this was a state change. """ # Note: Some callers check _cached_byte_size_dirty before calling # _Modified() as an extra optimization. So, if this method is ever # changed such that it does stuff even when _cached_byte_size_dirty is # already true, the callers need to be updated. if not self._cached_byte_size_dirty: self._cached_byte_size_dirty = True self._listener_for_children.dirty = True self._is_present_in_parent = True self._listener.Modified() def _UpdateOneofState(self, field): """Sets field as the active field in its containing oneof. Will also delete currently active field in the oneof, if it is different from the argument. Does not mark the message as modified. """ other_field = self._oneofs.setdefault(field.containing_oneof, field) if other_field is not field: del self._fields[other_field] self._oneofs[field.containing_oneof] = field cls._Modified = Modified cls.SetInParent = Modified cls._UpdateOneofState = _UpdateOneofState class _Listener(object): """MessageListener implementation that a parent message registers with its child message. In order to support semantics like: foo.bar.baz.qux = 23 assert foo.HasField('bar') ...child objects must have back references to their parents. This helper class is at the heart of this support. """ def __init__(self, parent_message): """Args: parent_message: The message whose _Modified() method we should call when we receive Modified() messages. """ # This listener establishes a back reference from a child (contained) object # to its parent (containing) object. We make this a weak reference to avoid # creating cyclic garbage when the client finishes with the 'parent' object # in the tree. if isinstance(parent_message, weakref.ProxyType): self._parent_message_weakref = parent_message else: self._parent_message_weakref = weakref.proxy(parent_message) # As an optimization, we also indicate directly on the listener whether # or not the parent message is dirty. This way we can avoid traversing # up the tree in the common case. self.dirty = False def Modified(self): if self.dirty: return try: # Propagate the signal to our parents iff this is the first field set. self._parent_message_weakref._Modified() except ReferenceError: # We can get here if a client has kept a reference to a child object, # and is now setting a field on it, but the child's parent has been # garbage-collected. This is not an error. pass class _OneofListener(_Listener): """Special listener implementation for setting composite oneof fields.""" def __init__(self, parent_message, field): """Args: parent_message: The message whose _Modified() method we should call when we receive Modified() messages. field: The descriptor of the field being set in the parent message. """ super(_OneofListener, self).__init__(parent_message) self._field = field def Modified(self): """Also updates the state of the containing oneof in the parent message.""" try: self._parent_message_weakref._UpdateOneofState(self._field) super(_OneofListener, self).Modified() except ReferenceError: pass ================================================ FILE: openpype/hosts/nuke/vendor/google/protobuf/internal/type_checkers.py ================================================ # Protocol Buffers - Google's data interchange format # Copyright 2008 Google Inc. All rights reserved. # https://developers.google.com/protocol-buffers/ # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are # met: # # * Redistributions of source code must retain the above copyright # notice, this list of conditions and the following disclaimer. # * Redistributions in binary form must reproduce the above # copyright notice, this list of conditions and the following disclaimer # in the documentation and/or other materials provided with the # distribution. # * Neither the name of Google Inc. nor the names of its # contributors may be used to endorse or promote products derived from # this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. """Provides type checking routines. This module defines type checking utilities in the forms of dictionaries: VALUE_CHECKERS: A dictionary of field types and a value validation object. TYPE_TO_BYTE_SIZE_FN: A dictionary with field types and a size computing function. TYPE_TO_SERIALIZE_METHOD: A dictionary with field types and serialization function. FIELD_TYPE_TO_WIRE_TYPE: A dictionary with field typed and their corresponding wire types. TYPE_TO_DESERIALIZE_METHOD: A dictionary with field types and deserialization function. """ __author__ = 'robinson@google.com (Will Robinson)' import ctypes import numbers from google.protobuf.internal import decoder from google.protobuf.internal import encoder from google.protobuf.internal import wire_format from google.protobuf import descriptor _FieldDescriptor = descriptor.FieldDescriptor def TruncateToFourByteFloat(original): return ctypes.c_float(original).value def ToShortestFloat(original): """Returns the shortest float that has same value in wire.""" # All 4 byte floats have between 6 and 9 significant digits, so we # start with 6 as the lower bound. # It has to be iterative because use '.9g' directly can not get rid # of the noises for most values. For example if set a float_field=0.9 # use '.9g' will print 0.899999976. precision = 6 rounded = float('{0:.{1}g}'.format(original, precision)) while TruncateToFourByteFloat(rounded) != original: precision += 1 rounded = float('{0:.{1}g}'.format(original, precision)) return rounded def SupportsOpenEnums(field_descriptor): return field_descriptor.containing_type.syntax == 'proto3' def GetTypeChecker(field): """Returns a type checker for a message field of the specified types. Args: field: FieldDescriptor object for this field. Returns: An instance of TypeChecker which can be used to verify the types of values assigned to a field of the specified type. """ if (field.cpp_type == _FieldDescriptor.CPPTYPE_STRING and field.type == _FieldDescriptor.TYPE_STRING): return UnicodeValueChecker() if field.cpp_type == _FieldDescriptor.CPPTYPE_ENUM: if SupportsOpenEnums(field): # When open enums are supported, any int32 can be assigned. return _VALUE_CHECKERS[_FieldDescriptor.CPPTYPE_INT32] else: return EnumValueChecker(field.enum_type) return _VALUE_CHECKERS[field.cpp_type] # None of the typecheckers below make any attempt to guard against people # subclassing builtin types and doing weird things. We're not trying to # protect against malicious clients here, just people accidentally shooting # themselves in the foot in obvious ways. class TypeChecker(object): """Type checker used to catch type errors as early as possible when the client is setting scalar fields in protocol messages. """ def __init__(self, *acceptable_types): self._acceptable_types = acceptable_types def CheckValue(self, proposed_value): """Type check the provided value and return it. The returned value might have been normalized to another type. """ if not isinstance(proposed_value, self._acceptable_types): message = ('%.1024r has type %s, but expected one of: %s' % (proposed_value, type(proposed_value), self._acceptable_types)) raise TypeError(message) return proposed_value class TypeCheckerWithDefault(TypeChecker): def __init__(self, default_value, *acceptable_types): TypeChecker.__init__(self, *acceptable_types) self._default_value = default_value def DefaultValue(self): return self._default_value class BoolValueChecker(object): """Type checker used for bool fields.""" def CheckValue(self, proposed_value): if not hasattr(proposed_value, '__index__') or ( type(proposed_value).__module__ == 'numpy' and type(proposed_value).__name__ == 'ndarray'): message = ('%.1024r has type %s, but expected one of: %s' % (proposed_value, type(proposed_value), (bool, int))) raise TypeError(message) return bool(proposed_value) def DefaultValue(self): return False # IntValueChecker and its subclasses perform integer type-checks # and bounds-checks. class IntValueChecker(object): """Checker used for integer fields. Performs type-check and range check.""" def CheckValue(self, proposed_value): if not hasattr(proposed_value, '__index__') or ( type(proposed_value).__module__ == 'numpy' and type(proposed_value).__name__ == 'ndarray'): message = ('%.1024r has type %s, but expected one of: %s' % (proposed_value, type(proposed_value), (int,))) raise TypeError(message) if not self._MIN <= int(proposed_value) <= self._MAX: raise ValueError('Value out of range: %d' % proposed_value) # We force all values to int to make alternate implementations where the # distinction is more significant (e.g. the C++ implementation) simpler. proposed_value = int(proposed_value) return proposed_value def DefaultValue(self): return 0 class EnumValueChecker(object): """Checker used for enum fields. Performs type-check and range check.""" def __init__(self, enum_type): self._enum_type = enum_type def CheckValue(self, proposed_value): if not isinstance(proposed_value, numbers.Integral): message = ('%.1024r has type %s, but expected one of: %s' % (proposed_value, type(proposed_value), (int,))) raise TypeError(message) if int(proposed_value) not in self._enum_type.values_by_number: raise ValueError('Unknown enum value: %d' % proposed_value) return proposed_value def DefaultValue(self): return self._enum_type.values[0].number class UnicodeValueChecker(object): """Checker used for string fields. Always returns a unicode value, even if the input is of type str. """ def CheckValue(self, proposed_value): if not isinstance(proposed_value, (bytes, str)): message = ('%.1024r has type %s, but expected one of: %s' % (proposed_value, type(proposed_value), (bytes, str))) raise TypeError(message) # If the value is of type 'bytes' make sure that it is valid UTF-8 data. if isinstance(proposed_value, bytes): try: proposed_value = proposed_value.decode('utf-8') except UnicodeDecodeError: raise ValueError('%.1024r has type bytes, but isn\'t valid UTF-8 ' 'encoding. Non-UTF-8 strings must be converted to ' 'unicode objects before being added.' % (proposed_value)) else: try: proposed_value.encode('utf8') except UnicodeEncodeError: raise ValueError('%.1024r isn\'t a valid unicode string and ' 'can\'t be encoded in UTF-8.'% (proposed_value)) return proposed_value def DefaultValue(self): return u"" class Int32ValueChecker(IntValueChecker): # We're sure to use ints instead of longs here since comparison may be more # efficient. _MIN = -2147483648 _MAX = 2147483647 class Uint32ValueChecker(IntValueChecker): _MIN = 0 _MAX = (1 << 32) - 1 class Int64ValueChecker(IntValueChecker): _MIN = -(1 << 63) _MAX = (1 << 63) - 1 class Uint64ValueChecker(IntValueChecker): _MIN = 0 _MAX = (1 << 64) - 1 # The max 4 bytes float is about 3.4028234663852886e+38 _FLOAT_MAX = float.fromhex('0x1.fffffep+127') _FLOAT_MIN = -_FLOAT_MAX _INF = float('inf') _NEG_INF = float('-inf') class DoubleValueChecker(object): """Checker used for double fields. Performs type-check and range check. """ def CheckValue(self, proposed_value): """Check and convert proposed_value to float.""" if (not hasattr(proposed_value, '__float__') and not hasattr(proposed_value, '__index__')) or ( type(proposed_value).__module__ == 'numpy' and type(proposed_value).__name__ == 'ndarray'): message = ('%.1024r has type %s, but expected one of: int, float' % (proposed_value, type(proposed_value))) raise TypeError(message) return float(proposed_value) def DefaultValue(self): return 0.0 class FloatValueChecker(DoubleValueChecker): """Checker used for float fields. Performs type-check and range check. Values exceeding a 32-bit float will be converted to inf/-inf. """ def CheckValue(self, proposed_value): """Check and convert proposed_value to float.""" converted_value = super().CheckValue(proposed_value) # This inf rounding matches the C++ proto SafeDoubleToFloat logic. if converted_value > _FLOAT_MAX: return _INF if converted_value < _FLOAT_MIN: return _NEG_INF return TruncateToFourByteFloat(converted_value) # Type-checkers for all scalar CPPTYPEs. _VALUE_CHECKERS = { _FieldDescriptor.CPPTYPE_INT32: Int32ValueChecker(), _FieldDescriptor.CPPTYPE_INT64: Int64ValueChecker(), _FieldDescriptor.CPPTYPE_UINT32: Uint32ValueChecker(), _FieldDescriptor.CPPTYPE_UINT64: Uint64ValueChecker(), _FieldDescriptor.CPPTYPE_DOUBLE: DoubleValueChecker(), _FieldDescriptor.CPPTYPE_FLOAT: FloatValueChecker(), _FieldDescriptor.CPPTYPE_BOOL: BoolValueChecker(), _FieldDescriptor.CPPTYPE_STRING: TypeCheckerWithDefault(b'', bytes), } # Map from field type to a function F, such that F(field_num, value) # gives the total byte size for a value of the given type. This # byte size includes tag information and any other additional space # associated with serializing "value". TYPE_TO_BYTE_SIZE_FN = { _FieldDescriptor.TYPE_DOUBLE: wire_format.DoubleByteSize, _FieldDescriptor.TYPE_FLOAT: wire_format.FloatByteSize, _FieldDescriptor.TYPE_INT64: wire_format.Int64ByteSize, _FieldDescriptor.TYPE_UINT64: wire_format.UInt64ByteSize, _FieldDescriptor.TYPE_INT32: wire_format.Int32ByteSize, _FieldDescriptor.TYPE_FIXED64: wire_format.Fixed64ByteSize, _FieldDescriptor.TYPE_FIXED32: wire_format.Fixed32ByteSize, _FieldDescriptor.TYPE_BOOL: wire_format.BoolByteSize, _FieldDescriptor.TYPE_STRING: wire_format.StringByteSize, _FieldDescriptor.TYPE_GROUP: wire_format.GroupByteSize, _FieldDescriptor.TYPE_MESSAGE: wire_format.MessageByteSize, _FieldDescriptor.TYPE_BYTES: wire_format.BytesByteSize, _FieldDescriptor.TYPE_UINT32: wire_format.UInt32ByteSize, _FieldDescriptor.TYPE_ENUM: wire_format.EnumByteSize, _FieldDescriptor.TYPE_SFIXED32: wire_format.SFixed32ByteSize, _FieldDescriptor.TYPE_SFIXED64: wire_format.SFixed64ByteSize, _FieldDescriptor.TYPE_SINT32: wire_format.SInt32ByteSize, _FieldDescriptor.TYPE_SINT64: wire_format.SInt64ByteSize } # Maps from field types to encoder constructors. TYPE_TO_ENCODER = { _FieldDescriptor.TYPE_DOUBLE: encoder.DoubleEncoder, _FieldDescriptor.TYPE_FLOAT: encoder.FloatEncoder, _FieldDescriptor.TYPE_INT64: encoder.Int64Encoder, _FieldDescriptor.TYPE_UINT64: encoder.UInt64Encoder, _FieldDescriptor.TYPE_INT32: encoder.Int32Encoder, _FieldDescriptor.TYPE_FIXED64: encoder.Fixed64Encoder, _FieldDescriptor.TYPE_FIXED32: encoder.Fixed32Encoder, _FieldDescriptor.TYPE_BOOL: encoder.BoolEncoder, _FieldDescriptor.TYPE_STRING: encoder.StringEncoder, _FieldDescriptor.TYPE_GROUP: encoder.GroupEncoder, _FieldDescriptor.TYPE_MESSAGE: encoder.MessageEncoder, _FieldDescriptor.TYPE_BYTES: encoder.BytesEncoder, _FieldDescriptor.TYPE_UINT32: encoder.UInt32Encoder, _FieldDescriptor.TYPE_ENUM: encoder.EnumEncoder, _FieldDescriptor.TYPE_SFIXED32: encoder.SFixed32Encoder, _FieldDescriptor.TYPE_SFIXED64: encoder.SFixed64Encoder, _FieldDescriptor.TYPE_SINT32: encoder.SInt32Encoder, _FieldDescriptor.TYPE_SINT64: encoder.SInt64Encoder, } # Maps from field types to sizer constructors. TYPE_TO_SIZER = { _FieldDescriptor.TYPE_DOUBLE: encoder.DoubleSizer, _FieldDescriptor.TYPE_FLOAT: encoder.FloatSizer, _FieldDescriptor.TYPE_INT64: encoder.Int64Sizer, _FieldDescriptor.TYPE_UINT64: encoder.UInt64Sizer, _FieldDescriptor.TYPE_INT32: encoder.Int32Sizer, _FieldDescriptor.TYPE_FIXED64: encoder.Fixed64Sizer, _FieldDescriptor.TYPE_FIXED32: encoder.Fixed32Sizer, _FieldDescriptor.TYPE_BOOL: encoder.BoolSizer, _FieldDescriptor.TYPE_STRING: encoder.StringSizer, _FieldDescriptor.TYPE_GROUP: encoder.GroupSizer, _FieldDescriptor.TYPE_MESSAGE: encoder.MessageSizer, _FieldDescriptor.TYPE_BYTES: encoder.BytesSizer, _FieldDescriptor.TYPE_UINT32: encoder.UInt32Sizer, _FieldDescriptor.TYPE_ENUM: encoder.EnumSizer, _FieldDescriptor.TYPE_SFIXED32: encoder.SFixed32Sizer, _FieldDescriptor.TYPE_SFIXED64: encoder.SFixed64Sizer, _FieldDescriptor.TYPE_SINT32: encoder.SInt32Sizer, _FieldDescriptor.TYPE_SINT64: encoder.SInt64Sizer, } # Maps from field type to a decoder constructor. TYPE_TO_DECODER = { _FieldDescriptor.TYPE_DOUBLE: decoder.DoubleDecoder, _FieldDescriptor.TYPE_FLOAT: decoder.FloatDecoder, _FieldDescriptor.TYPE_INT64: decoder.Int64Decoder, _FieldDescriptor.TYPE_UINT64: decoder.UInt64Decoder, _FieldDescriptor.TYPE_INT32: decoder.Int32Decoder, _FieldDescriptor.TYPE_FIXED64: decoder.Fixed64Decoder, _FieldDescriptor.TYPE_FIXED32: decoder.Fixed32Decoder, _FieldDescriptor.TYPE_BOOL: decoder.BoolDecoder, _FieldDescriptor.TYPE_STRING: decoder.StringDecoder, _FieldDescriptor.TYPE_GROUP: decoder.GroupDecoder, _FieldDescriptor.TYPE_MESSAGE: decoder.MessageDecoder, _FieldDescriptor.TYPE_BYTES: decoder.BytesDecoder, _FieldDescriptor.TYPE_UINT32: decoder.UInt32Decoder, _FieldDescriptor.TYPE_ENUM: decoder.EnumDecoder, _FieldDescriptor.TYPE_SFIXED32: decoder.SFixed32Decoder, _FieldDescriptor.TYPE_SFIXED64: decoder.SFixed64Decoder, _FieldDescriptor.TYPE_SINT32: decoder.SInt32Decoder, _FieldDescriptor.TYPE_SINT64: decoder.SInt64Decoder, } # Maps from field type to expected wiretype. FIELD_TYPE_TO_WIRE_TYPE = { _FieldDescriptor.TYPE_DOUBLE: wire_format.WIRETYPE_FIXED64, _FieldDescriptor.TYPE_FLOAT: wire_format.WIRETYPE_FIXED32, _FieldDescriptor.TYPE_INT64: wire_format.WIRETYPE_VARINT, _FieldDescriptor.TYPE_UINT64: wire_format.WIRETYPE_VARINT, _FieldDescriptor.TYPE_INT32: wire_format.WIRETYPE_VARINT, _FieldDescriptor.TYPE_FIXED64: wire_format.WIRETYPE_FIXED64, _FieldDescriptor.TYPE_FIXED32: wire_format.WIRETYPE_FIXED32, _FieldDescriptor.TYPE_BOOL: wire_format.WIRETYPE_VARINT, _FieldDescriptor.TYPE_STRING: wire_format.WIRETYPE_LENGTH_DELIMITED, _FieldDescriptor.TYPE_GROUP: wire_format.WIRETYPE_START_GROUP, _FieldDescriptor.TYPE_MESSAGE: wire_format.WIRETYPE_LENGTH_DELIMITED, _FieldDescriptor.TYPE_BYTES: wire_format.WIRETYPE_LENGTH_DELIMITED, _FieldDescriptor.TYPE_UINT32: wire_format.WIRETYPE_VARINT, _FieldDescriptor.TYPE_ENUM: wire_format.WIRETYPE_VARINT, _FieldDescriptor.TYPE_SFIXED32: wire_format.WIRETYPE_FIXED32, _FieldDescriptor.TYPE_SFIXED64: wire_format.WIRETYPE_FIXED64, _FieldDescriptor.TYPE_SINT32: wire_format.WIRETYPE_VARINT, _FieldDescriptor.TYPE_SINT64: wire_format.WIRETYPE_VARINT, } ================================================ FILE: openpype/hosts/nuke/vendor/google/protobuf/internal/well_known_types.py ================================================ # Protocol Buffers - Google's data interchange format # Copyright 2008 Google Inc. All rights reserved. # https://developers.google.com/protocol-buffers/ # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are # met: # # * Redistributions of source code must retain the above copyright # notice, this list of conditions and the following disclaimer. # * Redistributions in binary form must reproduce the above # copyright notice, this list of conditions and the following disclaimer # in the documentation and/or other materials provided with the # distribution. # * Neither the name of Google Inc. nor the names of its # contributors may be used to endorse or promote products derived from # this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. """Contains well known classes. This files defines well known classes which need extra maintenance including: - Any - Duration - FieldMask - Struct - Timestamp """ __author__ = 'jieluo@google.com (Jie Luo)' import calendar import collections.abc import datetime from google.protobuf.descriptor import FieldDescriptor _TIMESTAMPFOMAT = '%Y-%m-%dT%H:%M:%S' _NANOS_PER_SECOND = 1000000000 _NANOS_PER_MILLISECOND = 1000000 _NANOS_PER_MICROSECOND = 1000 _MILLIS_PER_SECOND = 1000 _MICROS_PER_SECOND = 1000000 _SECONDS_PER_DAY = 24 * 3600 _DURATION_SECONDS_MAX = 315576000000 class Any(object): """Class for Any Message type.""" __slots__ = () def Pack(self, msg, type_url_prefix='type.googleapis.com/', deterministic=None): """Packs the specified message into current Any message.""" if len(type_url_prefix) < 1 or type_url_prefix[-1] != '/': self.type_url = '%s/%s' % (type_url_prefix, msg.DESCRIPTOR.full_name) else: self.type_url = '%s%s' % (type_url_prefix, msg.DESCRIPTOR.full_name) self.value = msg.SerializeToString(deterministic=deterministic) def Unpack(self, msg): """Unpacks the current Any message into specified message.""" descriptor = msg.DESCRIPTOR if not self.Is(descriptor): return False msg.ParseFromString(self.value) return True def TypeName(self): """Returns the protobuf type name of the inner message.""" # Only last part is to be used: b/25630112 return self.type_url.split('/')[-1] def Is(self, descriptor): """Checks if this Any represents the given protobuf type.""" return '/' in self.type_url and self.TypeName() == descriptor.full_name _EPOCH_DATETIME_NAIVE = datetime.datetime.utcfromtimestamp(0) _EPOCH_DATETIME_AWARE = datetime.datetime.fromtimestamp( 0, tz=datetime.timezone.utc) class Timestamp(object): """Class for Timestamp message type.""" __slots__ = () def ToJsonString(self): """Converts Timestamp to RFC 3339 date string format. Returns: A string converted from timestamp. The string is always Z-normalized and uses 3, 6 or 9 fractional digits as required to represent the exact time. Example of the return format: '1972-01-01T10:00:20.021Z' """ nanos = self.nanos % _NANOS_PER_SECOND total_sec = self.seconds + (self.nanos - nanos) // _NANOS_PER_SECOND seconds = total_sec % _SECONDS_PER_DAY days = (total_sec - seconds) // _SECONDS_PER_DAY dt = datetime.datetime(1970, 1, 1) + datetime.timedelta(days, seconds) result = dt.isoformat() if (nanos % 1e9) == 0: # If there are 0 fractional digits, the fractional # point '.' should be omitted when serializing. return result + 'Z' if (nanos % 1e6) == 0: # Serialize 3 fractional digits. return result + '.%03dZ' % (nanos / 1e6) if (nanos % 1e3) == 0: # Serialize 6 fractional digits. return result + '.%06dZ' % (nanos / 1e3) # Serialize 9 fractional digits. return result + '.%09dZ' % nanos def FromJsonString(self, value): """Parse a RFC 3339 date string format to Timestamp. Args: value: A date string. Any fractional digits (or none) and any offset are accepted as long as they fit into nano-seconds precision. Example of accepted format: '1972-01-01T10:00:20.021-05:00' Raises: ValueError: On parsing problems. """ if not isinstance(value, str): raise ValueError('Timestamp JSON value not a string: {!r}'.format(value)) timezone_offset = value.find('Z') if timezone_offset == -1: timezone_offset = value.find('+') if timezone_offset == -1: timezone_offset = value.rfind('-') if timezone_offset == -1: raise ValueError( 'Failed to parse timestamp: missing valid timezone offset.') time_value = value[0:timezone_offset] # Parse datetime and nanos. point_position = time_value.find('.') if point_position == -1: second_value = time_value nano_value = '' else: second_value = time_value[:point_position] nano_value = time_value[point_position + 1:] if 't' in second_value: raise ValueError( 'time data \'{0}\' does not match format \'%Y-%m-%dT%H:%M:%S\', ' 'lowercase \'t\' is not accepted'.format(second_value)) date_object = datetime.datetime.strptime(second_value, _TIMESTAMPFOMAT) td = date_object - datetime.datetime(1970, 1, 1) seconds = td.seconds + td.days * _SECONDS_PER_DAY if len(nano_value) > 9: raise ValueError( 'Failed to parse Timestamp: nanos {0} more than ' '9 fractional digits.'.format(nano_value)) if nano_value: nanos = round(float('0.' + nano_value) * 1e9) else: nanos = 0 # Parse timezone offsets. if value[timezone_offset] == 'Z': if len(value) != timezone_offset + 1: raise ValueError('Failed to parse timestamp: invalid trailing' ' data {0}.'.format(value)) else: timezone = value[timezone_offset:] pos = timezone.find(':') if pos == -1: raise ValueError( 'Invalid timezone offset value: {0}.'.format(timezone)) if timezone[0] == '+': seconds -= (int(timezone[1:pos])*60+int(timezone[pos+1:]))*60 else: seconds += (int(timezone[1:pos])*60+int(timezone[pos+1:]))*60 # Set seconds and nanos self.seconds = int(seconds) self.nanos = int(nanos) def GetCurrentTime(self): """Get the current UTC into Timestamp.""" self.FromDatetime(datetime.datetime.utcnow()) def ToNanoseconds(self): """Converts Timestamp to nanoseconds since epoch.""" return self.seconds * _NANOS_PER_SECOND + self.nanos def ToMicroseconds(self): """Converts Timestamp to microseconds since epoch.""" return (self.seconds * _MICROS_PER_SECOND + self.nanos // _NANOS_PER_MICROSECOND) def ToMilliseconds(self): """Converts Timestamp to milliseconds since epoch.""" return (self.seconds * _MILLIS_PER_SECOND + self.nanos // _NANOS_PER_MILLISECOND) def ToSeconds(self): """Converts Timestamp to seconds since epoch.""" return self.seconds def FromNanoseconds(self, nanos): """Converts nanoseconds since epoch to Timestamp.""" self.seconds = nanos // _NANOS_PER_SECOND self.nanos = nanos % _NANOS_PER_SECOND def FromMicroseconds(self, micros): """Converts microseconds since epoch to Timestamp.""" self.seconds = micros // _MICROS_PER_SECOND self.nanos = (micros % _MICROS_PER_SECOND) * _NANOS_PER_MICROSECOND def FromMilliseconds(self, millis): """Converts milliseconds since epoch to Timestamp.""" self.seconds = millis // _MILLIS_PER_SECOND self.nanos = (millis % _MILLIS_PER_SECOND) * _NANOS_PER_MILLISECOND def FromSeconds(self, seconds): """Converts seconds since epoch to Timestamp.""" self.seconds = seconds self.nanos = 0 def ToDatetime(self, tzinfo=None): """Converts Timestamp to a datetime. Args: tzinfo: A datetime.tzinfo subclass; defaults to None. Returns: If tzinfo is None, returns a timezone-naive UTC datetime (with no timezone information, i.e. not aware that it's UTC). Otherwise, returns a timezone-aware datetime in the input timezone. """ delta = datetime.timedelta( seconds=self.seconds, microseconds=_RoundTowardZero(self.nanos, _NANOS_PER_MICROSECOND)) if tzinfo is None: return _EPOCH_DATETIME_NAIVE + delta else: return _EPOCH_DATETIME_AWARE.astimezone(tzinfo) + delta def FromDatetime(self, dt): """Converts datetime to Timestamp. Args: dt: A datetime. If it's timezone-naive, it's assumed to be in UTC. """ # Using this guide: http://wiki.python.org/moin/WorkingWithTime # And this conversion guide: http://docs.python.org/library/time.html # Turn the date parameter into a tuple (struct_time) that can then be # manipulated into a long value of seconds. During the conversion from # struct_time to long, the source date in UTC, and so it follows that the # correct transformation is calendar.timegm() self.seconds = calendar.timegm(dt.utctimetuple()) self.nanos = dt.microsecond * _NANOS_PER_MICROSECOND class Duration(object): """Class for Duration message type.""" __slots__ = () def ToJsonString(self): """Converts Duration to string format. Returns: A string converted from self. The string format will contains 3, 6, or 9 fractional digits depending on the precision required to represent the exact Duration value. For example: "1s", "1.010s", "1.000000100s", "-3.100s" """ _CheckDurationValid(self.seconds, self.nanos) if self.seconds < 0 or self.nanos < 0: result = '-' seconds = - self.seconds + int((0 - self.nanos) // 1e9) nanos = (0 - self.nanos) % 1e9 else: result = '' seconds = self.seconds + int(self.nanos // 1e9) nanos = self.nanos % 1e9 result += '%d' % seconds if (nanos % 1e9) == 0: # If there are 0 fractional digits, the fractional # point '.' should be omitted when serializing. return result + 's' if (nanos % 1e6) == 0: # Serialize 3 fractional digits. return result + '.%03ds' % (nanos / 1e6) if (nanos % 1e3) == 0: # Serialize 6 fractional digits. return result + '.%06ds' % (nanos / 1e3) # Serialize 9 fractional digits. return result + '.%09ds' % nanos def FromJsonString(self, value): """Converts a string to Duration. Args: value: A string to be converted. The string must end with 's'. Any fractional digits (or none) are accepted as long as they fit into precision. For example: "1s", "1.01s", "1.0000001s", "-3.100s Raises: ValueError: On parsing problems. """ if not isinstance(value, str): raise ValueError('Duration JSON value not a string: {!r}'.format(value)) if len(value) < 1 or value[-1] != 's': raise ValueError( 'Duration must end with letter "s": {0}.'.format(value)) try: pos = value.find('.') if pos == -1: seconds = int(value[:-1]) nanos = 0 else: seconds = int(value[:pos]) if value[0] == '-': nanos = int(round(float('-0{0}'.format(value[pos: -1])) *1e9)) else: nanos = int(round(float('0{0}'.format(value[pos: -1])) *1e9)) _CheckDurationValid(seconds, nanos) self.seconds = seconds self.nanos = nanos except ValueError as e: raise ValueError( 'Couldn\'t parse duration: {0} : {1}.'.format(value, e)) def ToNanoseconds(self): """Converts a Duration to nanoseconds.""" return self.seconds * _NANOS_PER_SECOND + self.nanos def ToMicroseconds(self): """Converts a Duration to microseconds.""" micros = _RoundTowardZero(self.nanos, _NANOS_PER_MICROSECOND) return self.seconds * _MICROS_PER_SECOND + micros def ToMilliseconds(self): """Converts a Duration to milliseconds.""" millis = _RoundTowardZero(self.nanos, _NANOS_PER_MILLISECOND) return self.seconds * _MILLIS_PER_SECOND + millis def ToSeconds(self): """Converts a Duration to seconds.""" return self.seconds def FromNanoseconds(self, nanos): """Converts nanoseconds to Duration.""" self._NormalizeDuration(nanos // _NANOS_PER_SECOND, nanos % _NANOS_PER_SECOND) def FromMicroseconds(self, micros): """Converts microseconds to Duration.""" self._NormalizeDuration( micros // _MICROS_PER_SECOND, (micros % _MICROS_PER_SECOND) * _NANOS_PER_MICROSECOND) def FromMilliseconds(self, millis): """Converts milliseconds to Duration.""" self._NormalizeDuration( millis // _MILLIS_PER_SECOND, (millis % _MILLIS_PER_SECOND) * _NANOS_PER_MILLISECOND) def FromSeconds(self, seconds): """Converts seconds to Duration.""" self.seconds = seconds self.nanos = 0 def ToTimedelta(self): """Converts Duration to timedelta.""" return datetime.timedelta( seconds=self.seconds, microseconds=_RoundTowardZero( self.nanos, _NANOS_PER_MICROSECOND)) def FromTimedelta(self, td): """Converts timedelta to Duration.""" self._NormalizeDuration(td.seconds + td.days * _SECONDS_PER_DAY, td.microseconds * _NANOS_PER_MICROSECOND) def _NormalizeDuration(self, seconds, nanos): """Set Duration by seconds and nanos.""" # Force nanos to be negative if the duration is negative. if seconds < 0 and nanos > 0: seconds += 1 nanos -= _NANOS_PER_SECOND self.seconds = seconds self.nanos = nanos def _CheckDurationValid(seconds, nanos): if seconds < -_DURATION_SECONDS_MAX or seconds > _DURATION_SECONDS_MAX: raise ValueError( 'Duration is not valid: Seconds {0} must be in range ' '[-315576000000, 315576000000].'.format(seconds)) if nanos <= -_NANOS_PER_SECOND or nanos >= _NANOS_PER_SECOND: raise ValueError( 'Duration is not valid: Nanos {0} must be in range ' '[-999999999, 999999999].'.format(nanos)) if (nanos < 0 and seconds > 0) or (nanos > 0 and seconds < 0): raise ValueError( 'Duration is not valid: Sign mismatch.') def _RoundTowardZero(value, divider): """Truncates the remainder part after division.""" # For some languages, the sign of the remainder is implementation # dependent if any of the operands is negative. Here we enforce # "rounded toward zero" semantics. For example, for (-5) / 2 an # implementation may give -3 as the result with the remainder being # 1. This function ensures we always return -2 (closer to zero). result = value // divider remainder = value % divider if result < 0 and remainder > 0: return result + 1 else: return result class FieldMask(object): """Class for FieldMask message type.""" __slots__ = () def ToJsonString(self): """Converts FieldMask to string according to proto3 JSON spec.""" camelcase_paths = [] for path in self.paths: camelcase_paths.append(_SnakeCaseToCamelCase(path)) return ','.join(camelcase_paths) def FromJsonString(self, value): """Converts string to FieldMask according to proto3 JSON spec.""" if not isinstance(value, str): raise ValueError('FieldMask JSON value not a string: {!r}'.format(value)) self.Clear() if value: for path in value.split(','): self.paths.append(_CamelCaseToSnakeCase(path)) def IsValidForDescriptor(self, message_descriptor): """Checks whether the FieldMask is valid for Message Descriptor.""" for path in self.paths: if not _IsValidPath(message_descriptor, path): return False return True def AllFieldsFromDescriptor(self, message_descriptor): """Gets all direct fields of Message Descriptor to FieldMask.""" self.Clear() for field in message_descriptor.fields: self.paths.append(field.name) def CanonicalFormFromMask(self, mask): """Converts a FieldMask to the canonical form. Removes paths that are covered by another path. For example, "foo.bar" is covered by "foo" and will be removed if "foo" is also in the FieldMask. Then sorts all paths in alphabetical order. Args: mask: The original FieldMask to be converted. """ tree = _FieldMaskTree(mask) tree.ToFieldMask(self) def Union(self, mask1, mask2): """Merges mask1 and mask2 into this FieldMask.""" _CheckFieldMaskMessage(mask1) _CheckFieldMaskMessage(mask2) tree = _FieldMaskTree(mask1) tree.MergeFromFieldMask(mask2) tree.ToFieldMask(self) def Intersect(self, mask1, mask2): """Intersects mask1 and mask2 into this FieldMask.""" _CheckFieldMaskMessage(mask1) _CheckFieldMaskMessage(mask2) tree = _FieldMaskTree(mask1) intersection = _FieldMaskTree() for path in mask2.paths: tree.IntersectPath(path, intersection) intersection.ToFieldMask(self) def MergeMessage( self, source, destination, replace_message_field=False, replace_repeated_field=False): """Merges fields specified in FieldMask from source to destination. Args: source: Source message. destination: The destination message to be merged into. replace_message_field: Replace message field if True. Merge message field if False. replace_repeated_field: Replace repeated field if True. Append elements of repeated field if False. """ tree = _FieldMaskTree(self) tree.MergeMessage( source, destination, replace_message_field, replace_repeated_field) def _IsValidPath(message_descriptor, path): """Checks whether the path is valid for Message Descriptor.""" parts = path.split('.') last = parts.pop() for name in parts: field = message_descriptor.fields_by_name.get(name) if (field is None or field.label == FieldDescriptor.LABEL_REPEATED or field.type != FieldDescriptor.TYPE_MESSAGE): return False message_descriptor = field.message_type return last in message_descriptor.fields_by_name def _CheckFieldMaskMessage(message): """Raises ValueError if message is not a FieldMask.""" message_descriptor = message.DESCRIPTOR if (message_descriptor.name != 'FieldMask' or message_descriptor.file.name != 'google/protobuf/field_mask.proto'): raise ValueError('Message {0} is not a FieldMask.'.format( message_descriptor.full_name)) def _SnakeCaseToCamelCase(path_name): """Converts a path name from snake_case to camelCase.""" result = [] after_underscore = False for c in path_name: if c.isupper(): raise ValueError( 'Fail to print FieldMask to Json string: Path name ' '{0} must not contain uppercase letters.'.format(path_name)) if after_underscore: if c.islower(): result.append(c.upper()) after_underscore = False else: raise ValueError( 'Fail to print FieldMask to Json string: The ' 'character after a "_" must be a lowercase letter ' 'in path name {0}.'.format(path_name)) elif c == '_': after_underscore = True else: result += c if after_underscore: raise ValueError('Fail to print FieldMask to Json string: Trailing "_" ' 'in path name {0}.'.format(path_name)) return ''.join(result) def _CamelCaseToSnakeCase(path_name): """Converts a field name from camelCase to snake_case.""" result = [] for c in path_name: if c == '_': raise ValueError('Fail to parse FieldMask: Path name ' '{0} must not contain "_"s.'.format(path_name)) if c.isupper(): result += '_' result += c.lower() else: result += c return ''.join(result) class _FieldMaskTree(object): """Represents a FieldMask in a tree structure. For example, given a FieldMask "foo.bar,foo.baz,bar.baz", the FieldMaskTree will be: [_root] -+- foo -+- bar | | | +- baz | +- bar --- baz In the tree, each leaf node represents a field path. """ __slots__ = ('_root',) def __init__(self, field_mask=None): """Initializes the tree by FieldMask.""" self._root = {} if field_mask: self.MergeFromFieldMask(field_mask) def MergeFromFieldMask(self, field_mask): """Merges a FieldMask to the tree.""" for path in field_mask.paths: self.AddPath(path) def AddPath(self, path): """Adds a field path into the tree. If the field path to add is a sub-path of an existing field path in the tree (i.e., a leaf node), it means the tree already matches the given path so nothing will be added to the tree. If the path matches an existing non-leaf node in the tree, that non-leaf node will be turned into a leaf node with all its children removed because the path matches all the node's children. Otherwise, a new path will be added. Args: path: The field path to add. """ node = self._root for name in path.split('.'): if name not in node: node[name] = {} elif not node[name]: # Pre-existing empty node implies we already have this entire tree. return node = node[name] # Remove any sub-trees we might have had. node.clear() def ToFieldMask(self, field_mask): """Converts the tree to a FieldMask.""" field_mask.Clear() _AddFieldPaths(self._root, '', field_mask) def IntersectPath(self, path, intersection): """Calculates the intersection part of a field path with this tree. Args: path: The field path to calculates. intersection: The out tree to record the intersection part. """ node = self._root for name in path.split('.'): if name not in node: return elif not node[name]: intersection.AddPath(path) return node = node[name] intersection.AddLeafNodes(path, node) def AddLeafNodes(self, prefix, node): """Adds leaf nodes begin with prefix to this tree.""" if not node: self.AddPath(prefix) for name in node: child_path = prefix + '.' + name self.AddLeafNodes(child_path, node[name]) def MergeMessage( self, source, destination, replace_message, replace_repeated): """Merge all fields specified by this tree from source to destination.""" _MergeMessage( self._root, source, destination, replace_message, replace_repeated) def _StrConvert(value): """Converts value to str if it is not.""" # This file is imported by c extension and some methods like ClearField # requires string for the field name. py2/py3 has different text # type and may use unicode. if not isinstance(value, str): return value.encode('utf-8') return value def _MergeMessage( node, source, destination, replace_message, replace_repeated): """Merge all fields specified by a sub-tree from source to destination.""" source_descriptor = source.DESCRIPTOR for name in node: child = node[name] field = source_descriptor.fields_by_name[name] if field is None: raise ValueError('Error: Can\'t find field {0} in message {1}.'.format( name, source_descriptor.full_name)) if child: # Sub-paths are only allowed for singular message fields. if (field.label == FieldDescriptor.LABEL_REPEATED or field.cpp_type != FieldDescriptor.CPPTYPE_MESSAGE): raise ValueError('Error: Field {0} in message {1} is not a singular ' 'message field and cannot have sub-fields.'.format( name, source_descriptor.full_name)) if source.HasField(name): _MergeMessage( child, getattr(source, name), getattr(destination, name), replace_message, replace_repeated) continue if field.label == FieldDescriptor.LABEL_REPEATED: if replace_repeated: destination.ClearField(_StrConvert(name)) repeated_source = getattr(source, name) repeated_destination = getattr(destination, name) repeated_destination.MergeFrom(repeated_source) else: if field.cpp_type == FieldDescriptor.CPPTYPE_MESSAGE: if replace_message: destination.ClearField(_StrConvert(name)) if source.HasField(name): getattr(destination, name).MergeFrom(getattr(source, name)) else: setattr(destination, name, getattr(source, name)) def _AddFieldPaths(node, prefix, field_mask): """Adds the field paths descended from node to field_mask.""" if not node and prefix: field_mask.paths.append(prefix) return for name in sorted(node): if prefix: child_path = prefix + '.' + name else: child_path = name _AddFieldPaths(node[name], child_path, field_mask) def _SetStructValue(struct_value, value): if value is None: struct_value.null_value = 0 elif isinstance(value, bool): # Note: this check must come before the number check because in Python # True and False are also considered numbers. struct_value.bool_value = value elif isinstance(value, str): struct_value.string_value = value elif isinstance(value, (int, float)): struct_value.number_value = value elif isinstance(value, (dict, Struct)): struct_value.struct_value.Clear() struct_value.struct_value.update(value) elif isinstance(value, (list, ListValue)): struct_value.list_value.Clear() struct_value.list_value.extend(value) else: raise ValueError('Unexpected type') def _GetStructValue(struct_value): which = struct_value.WhichOneof('kind') if which == 'struct_value': return struct_value.struct_value elif which == 'null_value': return None elif which == 'number_value': return struct_value.number_value elif which == 'string_value': return struct_value.string_value elif which == 'bool_value': return struct_value.bool_value elif which == 'list_value': return struct_value.list_value elif which is None: raise ValueError('Value not set') class Struct(object): """Class for Struct message type.""" __slots__ = () def __getitem__(self, key): return _GetStructValue(self.fields[key]) def __contains__(self, item): return item in self.fields def __setitem__(self, key, value): _SetStructValue(self.fields[key], value) def __delitem__(self, key): del self.fields[key] def __len__(self): return len(self.fields) def __iter__(self): return iter(self.fields) def keys(self): # pylint: disable=invalid-name return self.fields.keys() def values(self): # pylint: disable=invalid-name return [self[key] for key in self] def items(self): # pylint: disable=invalid-name return [(key, self[key]) for key in self] def get_or_create_list(self, key): """Returns a list for this key, creating if it didn't exist already.""" if not self.fields[key].HasField('list_value'): # Clear will mark list_value modified which will indeed create a list. self.fields[key].list_value.Clear() return self.fields[key].list_value def get_or_create_struct(self, key): """Returns a struct for this key, creating if it didn't exist already.""" if not self.fields[key].HasField('struct_value'): # Clear will mark struct_value modified which will indeed create a struct. self.fields[key].struct_value.Clear() return self.fields[key].struct_value def update(self, dictionary): # pylint: disable=invalid-name for key, value in dictionary.items(): _SetStructValue(self.fields[key], value) collections.abc.MutableMapping.register(Struct) class ListValue(object): """Class for ListValue message type.""" __slots__ = () def __len__(self): return len(self.values) def append(self, value): _SetStructValue(self.values.add(), value) def extend(self, elem_seq): for value in elem_seq: self.append(value) def __getitem__(self, index): """Retrieves item by the specified index.""" return _GetStructValue(self.values.__getitem__(index)) def __setitem__(self, index, value): _SetStructValue(self.values.__getitem__(index), value) def __delitem__(self, key): del self.values[key] def items(self): for i in range(len(self)): yield self[i] def add_struct(self): """Appends and returns a struct value as the next value in the list.""" struct_value = self.values.add().struct_value # Clear will mark struct_value modified which will indeed create a struct. struct_value.Clear() return struct_value def add_list(self): """Appends and returns a list value as the next value in the list.""" list_value = self.values.add().list_value # Clear will mark list_value modified which will indeed create a list. list_value.Clear() return list_value collections.abc.MutableSequence.register(ListValue) WKTBASES = { 'google.protobuf.Any': Any, 'google.protobuf.Duration': Duration, 'google.protobuf.FieldMask': FieldMask, 'google.protobuf.ListValue': ListValue, 'google.protobuf.Struct': Struct, 'google.protobuf.Timestamp': Timestamp, } ================================================ FILE: openpype/hosts/nuke/vendor/google/protobuf/internal/wire_format.py ================================================ # Protocol Buffers - Google's data interchange format # Copyright 2008 Google Inc. All rights reserved. # https://developers.google.com/protocol-buffers/ # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are # met: # # * Redistributions of source code must retain the above copyright # notice, this list of conditions and the following disclaimer. # * Redistributions in binary form must reproduce the above # copyright notice, this list of conditions and the following disclaimer # in the documentation and/or other materials provided with the # distribution. # * Neither the name of Google Inc. nor the names of its # contributors may be used to endorse or promote products derived from # this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. """Constants and static functions to support protocol buffer wire format.""" __author__ = 'robinson@google.com (Will Robinson)' import struct from google.protobuf import descriptor from google.protobuf import message TAG_TYPE_BITS = 3 # Number of bits used to hold type info in a proto tag. TAG_TYPE_MASK = (1 << TAG_TYPE_BITS) - 1 # 0x7 # These numbers identify the wire type of a protocol buffer value. # We use the least-significant TAG_TYPE_BITS bits of the varint-encoded # tag-and-type to store one of these WIRETYPE_* constants. # These values must match WireType enum in google/protobuf/wire_format.h. WIRETYPE_VARINT = 0 WIRETYPE_FIXED64 = 1 WIRETYPE_LENGTH_DELIMITED = 2 WIRETYPE_START_GROUP = 3 WIRETYPE_END_GROUP = 4 WIRETYPE_FIXED32 = 5 _WIRETYPE_MAX = 5 # Bounds for various integer types. INT32_MAX = int((1 << 31) - 1) INT32_MIN = int(-(1 << 31)) UINT32_MAX = (1 << 32) - 1 INT64_MAX = (1 << 63) - 1 INT64_MIN = -(1 << 63) UINT64_MAX = (1 << 64) - 1 # "struct" format strings that will encode/decode the specified formats. FORMAT_UINT32_LITTLE_ENDIAN = '> TAG_TYPE_BITS), (tag & TAG_TYPE_MASK) def ZigZagEncode(value): """ZigZag Transform: Encodes signed integers so that they can be effectively used with varint encoding. See wire_format.h for more details. """ if value >= 0: return value << 1 return (value << 1) ^ (~0) def ZigZagDecode(value): """Inverse of ZigZagEncode().""" if not value & 0x1: return value >> 1 return (value >> 1) ^ (~0) # The *ByteSize() functions below return the number of bytes required to # serialize "field number + type" information and then serialize the value. def Int32ByteSize(field_number, int32): return Int64ByteSize(field_number, int32) def Int32ByteSizeNoTag(int32): return _VarUInt64ByteSizeNoTag(0xffffffffffffffff & int32) def Int64ByteSize(field_number, int64): # Have to convert to uint before calling UInt64ByteSize(). return UInt64ByteSize(field_number, 0xffffffffffffffff & int64) def UInt32ByteSize(field_number, uint32): return UInt64ByteSize(field_number, uint32) def UInt64ByteSize(field_number, uint64): return TagByteSize(field_number) + _VarUInt64ByteSizeNoTag(uint64) def SInt32ByteSize(field_number, int32): return UInt32ByteSize(field_number, ZigZagEncode(int32)) def SInt64ByteSize(field_number, int64): return UInt64ByteSize(field_number, ZigZagEncode(int64)) def Fixed32ByteSize(field_number, fixed32): return TagByteSize(field_number) + 4 def Fixed64ByteSize(field_number, fixed64): return TagByteSize(field_number) + 8 def SFixed32ByteSize(field_number, sfixed32): return TagByteSize(field_number) + 4 def SFixed64ByteSize(field_number, sfixed64): return TagByteSize(field_number) + 8 def FloatByteSize(field_number, flt): return TagByteSize(field_number) + 4 def DoubleByteSize(field_number, double): return TagByteSize(field_number) + 8 def BoolByteSize(field_number, b): return TagByteSize(field_number) + 1 def EnumByteSize(field_number, enum): return UInt32ByteSize(field_number, enum) def StringByteSize(field_number, string): return BytesByteSize(field_number, string.encode('utf-8')) def BytesByteSize(field_number, b): return (TagByteSize(field_number) + _VarUInt64ByteSizeNoTag(len(b)) + len(b)) def GroupByteSize(field_number, message): return (2 * TagByteSize(field_number) # START and END group. + message.ByteSize()) def MessageByteSize(field_number, message): return (TagByteSize(field_number) + _VarUInt64ByteSizeNoTag(message.ByteSize()) + message.ByteSize()) def MessageSetItemByteSize(field_number, msg): # First compute the sizes of the tags. # There are 2 tags for the beginning and ending of the repeated group, that # is field number 1, one with field number 2 (type_id) and one with field # number 3 (message). total_size = (2 * TagByteSize(1) + TagByteSize(2) + TagByteSize(3)) # Add the number of bytes for type_id. total_size += _VarUInt64ByteSizeNoTag(field_number) message_size = msg.ByteSize() # The number of bytes for encoding the length of the message. total_size += _VarUInt64ByteSizeNoTag(message_size) # The size of the message. total_size += message_size return total_size def TagByteSize(field_number): """Returns the bytes required to serialize a tag with this field number.""" # Just pass in type 0, since the type won't affect the tag+type size. return _VarUInt64ByteSizeNoTag(PackTag(field_number, 0)) # Private helper function for the *ByteSize() functions above. def _VarUInt64ByteSizeNoTag(uint64): """Returns the number of bytes required to serialize a single varint using boundary value comparisons. (unrolled loop optimization -WPierce) uint64 must be unsigned. """ if uint64 <= 0x7f: return 1 if uint64 <= 0x3fff: return 2 if uint64 <= 0x1fffff: return 3 if uint64 <= 0xfffffff: return 4 if uint64 <= 0x7ffffffff: return 5 if uint64 <= 0x3ffffffffff: return 6 if uint64 <= 0x1ffffffffffff: return 7 if uint64 <= 0xffffffffffffff: return 8 if uint64 <= 0x7fffffffffffffff: return 9 if uint64 > UINT64_MAX: raise message.EncodeError('Value out of range: %d' % uint64) return 10 NON_PACKABLE_TYPES = ( descriptor.FieldDescriptor.TYPE_STRING, descriptor.FieldDescriptor.TYPE_GROUP, descriptor.FieldDescriptor.TYPE_MESSAGE, descriptor.FieldDescriptor.TYPE_BYTES ) def IsTypePackable(field_type): """Return true iff packable = true is valid for fields of this type. Args: field_type: a FieldDescriptor::Type value. Returns: True iff fields of this type are packable. """ return field_type not in NON_PACKABLE_TYPES ================================================ FILE: openpype/hosts/nuke/vendor/google/protobuf/json_format.py ================================================ # Protocol Buffers - Google's data interchange format # Copyright 2008 Google Inc. All rights reserved. # https://developers.google.com/protocol-buffers/ # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are # met: # # * Redistributions of source code must retain the above copyright # notice, this list of conditions and the following disclaimer. # * Redistributions in binary form must reproduce the above # copyright notice, this list of conditions and the following disclaimer # in the documentation and/or other materials provided with the # distribution. # * Neither the name of Google Inc. nor the names of its # contributors may be used to endorse or promote products derived from # this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. """Contains routines for printing protocol messages in JSON format. Simple usage example: # Create a proto object and serialize it to a json format string. message = my_proto_pb2.MyMessage(foo='bar') json_string = json_format.MessageToJson(message) # Parse a json format string to proto object. message = json_format.Parse(json_string, my_proto_pb2.MyMessage()) """ __author__ = 'jieluo@google.com (Jie Luo)' import base64 from collections import OrderedDict import json import math from operator import methodcaller import re import sys from google.protobuf.internal import type_checkers from google.protobuf import descriptor from google.protobuf import symbol_database _TIMESTAMPFOMAT = '%Y-%m-%dT%H:%M:%S' _INT_TYPES = frozenset([descriptor.FieldDescriptor.CPPTYPE_INT32, descriptor.FieldDescriptor.CPPTYPE_UINT32, descriptor.FieldDescriptor.CPPTYPE_INT64, descriptor.FieldDescriptor.CPPTYPE_UINT64]) _INT64_TYPES = frozenset([descriptor.FieldDescriptor.CPPTYPE_INT64, descriptor.FieldDescriptor.CPPTYPE_UINT64]) _FLOAT_TYPES = frozenset([descriptor.FieldDescriptor.CPPTYPE_FLOAT, descriptor.FieldDescriptor.CPPTYPE_DOUBLE]) _INFINITY = 'Infinity' _NEG_INFINITY = '-Infinity' _NAN = 'NaN' _UNPAIRED_SURROGATE_PATTERN = re.compile( u'[\ud800-\udbff](?![\udc00-\udfff])|(? self.max_recursion_depth: raise ParseError('Message too deep. Max recursion depth is {0}'.format( self.max_recursion_depth)) message_descriptor = message.DESCRIPTOR full_name = message_descriptor.full_name if not path: path = message_descriptor.name if _IsWrapperMessage(message_descriptor): self._ConvertWrapperMessage(value, message, path) elif full_name in _WKTJSONMETHODS: methodcaller(_WKTJSONMETHODS[full_name][1], value, message, path)(self) else: self._ConvertFieldValuePair(value, message, path) self.recursion_depth -= 1 def _ConvertFieldValuePair(self, js, message, path): """Convert field value pairs into regular message. Args: js: A JSON object to convert the field value pairs. message: A regular protocol message to record the data. path: parent path to log parse error info. Raises: ParseError: In case of problems converting. """ names = [] message_descriptor = message.DESCRIPTOR fields_by_json_name = dict((f.json_name, f) for f in message_descriptor.fields) for name in js: try: field = fields_by_json_name.get(name, None) if not field: field = message_descriptor.fields_by_name.get(name, None) if not field and _VALID_EXTENSION_NAME.match(name): if not message_descriptor.is_extendable: raise ParseError( 'Message type {0} does not have extensions at {1}'.format( message_descriptor.full_name, path)) identifier = name[1:-1] # strip [] brackets # pylint: disable=protected-access field = message.Extensions._FindExtensionByName(identifier) # pylint: enable=protected-access if not field: # Try looking for extension by the message type name, dropping the # field name following the final . separator in full_name. identifier = '.'.join(identifier.split('.')[:-1]) # pylint: disable=protected-access field = message.Extensions._FindExtensionByName(identifier) # pylint: enable=protected-access if not field: if self.ignore_unknown_fields: continue raise ParseError( ('Message type "{0}" has no field named "{1}" at "{2}".\n' ' Available Fields(except extensions): "{3}"').format( message_descriptor.full_name, name, path, [f.json_name for f in message_descriptor.fields])) if name in names: raise ParseError('Message type "{0}" should not have multiple ' '"{1}" fields at "{2}".'.format( message.DESCRIPTOR.full_name, name, path)) names.append(name) value = js[name] # Check no other oneof field is parsed. if field.containing_oneof is not None and value is not None: oneof_name = field.containing_oneof.name if oneof_name in names: raise ParseError('Message type "{0}" should not have multiple ' '"{1}" oneof fields at "{2}".'.format( message.DESCRIPTOR.full_name, oneof_name, path)) names.append(oneof_name) if value is None: if (field.cpp_type == descriptor.FieldDescriptor.CPPTYPE_MESSAGE and field.message_type.full_name == 'google.protobuf.Value'): sub_message = getattr(message, field.name) sub_message.null_value = 0 elif (field.cpp_type == descriptor.FieldDescriptor.CPPTYPE_ENUM and field.enum_type.full_name == 'google.protobuf.NullValue'): setattr(message, field.name, 0) else: message.ClearField(field.name) continue # Parse field value. if _IsMapEntry(field): message.ClearField(field.name) self._ConvertMapFieldValue(value, message, field, '{0}.{1}'.format(path, name)) elif field.label == descriptor.FieldDescriptor.LABEL_REPEATED: message.ClearField(field.name) if not isinstance(value, list): raise ParseError('repeated field {0} must be in [] which is ' '{1} at {2}'.format(name, value, path)) if field.cpp_type == descriptor.FieldDescriptor.CPPTYPE_MESSAGE: # Repeated message field. for index, item in enumerate(value): sub_message = getattr(message, field.name).add() # None is a null_value in Value. if (item is None and sub_message.DESCRIPTOR.full_name != 'google.protobuf.Value'): raise ParseError('null is not allowed to be used as an element' ' in a repeated field at {0}.{1}[{2}]'.format( path, name, index)) self.ConvertMessage(item, sub_message, '{0}.{1}[{2}]'.format(path, name, index)) else: # Repeated scalar field. for index, item in enumerate(value): if item is None: raise ParseError('null is not allowed to be used as an element' ' in a repeated field at {0}.{1}[{2}]'.format( path, name, index)) getattr(message, field.name).append( _ConvertScalarFieldValue( item, field, '{0}.{1}[{2}]'.format(path, name, index))) elif field.cpp_type == descriptor.FieldDescriptor.CPPTYPE_MESSAGE: if field.is_extension: sub_message = message.Extensions[field] else: sub_message = getattr(message, field.name) sub_message.SetInParent() self.ConvertMessage(value, sub_message, '{0}.{1}'.format(path, name)) else: if field.is_extension: message.Extensions[field] = _ConvertScalarFieldValue( value, field, '{0}.{1}'.format(path, name)) else: setattr( message, field.name, _ConvertScalarFieldValue(value, field, '{0}.{1}'.format(path, name))) except ParseError as e: if field and field.containing_oneof is None: raise ParseError('Failed to parse {0} field: {1}.'.format(name, e)) else: raise ParseError(str(e)) except ValueError as e: raise ParseError('Failed to parse {0} field: {1}.'.format(name, e)) except TypeError as e: raise ParseError('Failed to parse {0} field: {1}.'.format(name, e)) def _ConvertAnyMessage(self, value, message, path): """Convert a JSON representation into Any message.""" if isinstance(value, dict) and not value: return try: type_url = value['@type'] except KeyError: raise ParseError( '@type is missing when parsing any message at {0}'.format(path)) try: sub_message = _CreateMessageFromTypeUrl(type_url, self.descriptor_pool) except TypeError as e: raise ParseError('{0} at {1}'.format(e, path)) message_descriptor = sub_message.DESCRIPTOR full_name = message_descriptor.full_name if _IsWrapperMessage(message_descriptor): self._ConvertWrapperMessage(value['value'], sub_message, '{0}.value'.format(path)) elif full_name in _WKTJSONMETHODS: methodcaller(_WKTJSONMETHODS[full_name][1], value['value'], sub_message, '{0}.value'.format(path))( self) else: del value['@type'] self._ConvertFieldValuePair(value, sub_message, path) value['@type'] = type_url # Sets Any message message.value = sub_message.SerializeToString() message.type_url = type_url def _ConvertGenericMessage(self, value, message, path): """Convert a JSON representation into message with FromJsonString.""" # Duration, Timestamp, FieldMask have a FromJsonString method to do the # conversion. Users can also call the method directly. try: message.FromJsonString(value) except ValueError as e: raise ParseError('{0} at {1}'.format(e, path)) def _ConvertValueMessage(self, value, message, path): """Convert a JSON representation into Value message.""" if isinstance(value, dict): self._ConvertStructMessage(value, message.struct_value, path) elif isinstance(value, list): self._ConvertListValueMessage(value, message.list_value, path) elif value is None: message.null_value = 0 elif isinstance(value, bool): message.bool_value = value elif isinstance(value, str): message.string_value = value elif isinstance(value, _INT_OR_FLOAT): message.number_value = value else: raise ParseError('Value {0} has unexpected type {1} at {2}'.format( value, type(value), path)) def _ConvertListValueMessage(self, value, message, path): """Convert a JSON representation into ListValue message.""" if not isinstance(value, list): raise ParseError('ListValue must be in [] which is {0} at {1}'.format( value, path)) message.ClearField('values') for index, item in enumerate(value): self._ConvertValueMessage(item, message.values.add(), '{0}[{1}]'.format(path, index)) def _ConvertStructMessage(self, value, message, path): """Convert a JSON representation into Struct message.""" if not isinstance(value, dict): raise ParseError('Struct must be in a dict which is {0} at {1}'.format( value, path)) # Clear will mark the struct as modified so it will be created even if # there are no values. message.Clear() for key in value: self._ConvertValueMessage(value[key], message.fields[key], '{0}.{1}'.format(path, key)) return def _ConvertWrapperMessage(self, value, message, path): """Convert a JSON representation into Wrapper message.""" field = message.DESCRIPTOR.fields_by_name['value'] setattr( message, 'value', _ConvertScalarFieldValue(value, field, path='{0}.value'.format(path))) def _ConvertMapFieldValue(self, value, message, field, path): """Convert map field value for a message map field. Args: value: A JSON object to convert the map field value. message: A protocol message to record the converted data. field: The descriptor of the map field to be converted. path: parent path to log parse error info. Raises: ParseError: In case of convert problems. """ if not isinstance(value, dict): raise ParseError( 'Map field {0} must be in a dict which is {1} at {2}'.format( field.name, value, path)) key_field = field.message_type.fields_by_name['key'] value_field = field.message_type.fields_by_name['value'] for key in value: key_value = _ConvertScalarFieldValue(key, key_field, '{0}.key'.format(path), True) if value_field.cpp_type == descriptor.FieldDescriptor.CPPTYPE_MESSAGE: self.ConvertMessage(value[key], getattr(message, field.name)[key_value], '{0}[{1}]'.format(path, key_value)) else: getattr(message, field.name)[key_value] = _ConvertScalarFieldValue( value[key], value_field, path='{0}[{1}]'.format(path, key_value)) def _ConvertScalarFieldValue(value, field, path, require_str=False): """Convert a single scalar field value. Args: value: A scalar value to convert the scalar field value. field: The descriptor of the field to convert. path: parent path to log parse error info. require_str: If True, the field value must be a str. Returns: The converted scalar field value Raises: ParseError: In case of convert problems. """ try: if field.cpp_type in _INT_TYPES: return _ConvertInteger(value) elif field.cpp_type in _FLOAT_TYPES: return _ConvertFloat(value, field) elif field.cpp_type == descriptor.FieldDescriptor.CPPTYPE_BOOL: return _ConvertBool(value, require_str) elif field.cpp_type == descriptor.FieldDescriptor.CPPTYPE_STRING: if field.type == descriptor.FieldDescriptor.TYPE_BYTES: if isinstance(value, str): encoded = value.encode('utf-8') else: encoded = value # Add extra padding '=' padded_value = encoded + b'=' * (4 - len(encoded) % 4) return base64.urlsafe_b64decode(padded_value) else: # Checking for unpaired surrogates appears to be unreliable, # depending on the specific Python version, so we check manually. if _UNPAIRED_SURROGATE_PATTERN.search(value): raise ParseError('Unpaired surrogate') return value elif field.cpp_type == descriptor.FieldDescriptor.CPPTYPE_ENUM: # Convert an enum value. enum_value = field.enum_type.values_by_name.get(value, None) if enum_value is None: try: number = int(value) enum_value = field.enum_type.values_by_number.get(number, None) except ValueError: raise ParseError('Invalid enum value {0} for enum type {1}'.format( value, field.enum_type.full_name)) if enum_value is None: if field.file.syntax == 'proto3': # Proto3 accepts unknown enums. return number raise ParseError('Invalid enum value {0} for enum type {1}'.format( value, field.enum_type.full_name)) return enum_value.number except ParseError as e: raise ParseError('{0} at {1}'.format(e, path)) def _ConvertInteger(value): """Convert an integer. Args: value: A scalar value to convert. Returns: The integer value. Raises: ParseError: If an integer couldn't be consumed. """ if isinstance(value, float) and not value.is_integer(): raise ParseError('Couldn\'t parse integer: {0}'.format(value)) if isinstance(value, str) and value.find(' ') != -1: raise ParseError('Couldn\'t parse integer: "{0}"'.format(value)) if isinstance(value, bool): raise ParseError('Bool value {0} is not acceptable for ' 'integer field'.format(value)) return int(value) def _ConvertFloat(value, field): """Convert an floating point number.""" if isinstance(value, float): if math.isnan(value): raise ParseError('Couldn\'t parse NaN, use quoted "NaN" instead') if math.isinf(value): if value > 0: raise ParseError('Couldn\'t parse Infinity or value too large, ' 'use quoted "Infinity" instead') else: raise ParseError('Couldn\'t parse -Infinity or value too small, ' 'use quoted "-Infinity" instead') if field.cpp_type == descriptor.FieldDescriptor.CPPTYPE_FLOAT: # pylint: disable=protected-access if value > type_checkers._FLOAT_MAX: raise ParseError('Float value too large') # pylint: disable=protected-access if value < type_checkers._FLOAT_MIN: raise ParseError('Float value too small') if value == 'nan': raise ParseError('Couldn\'t parse float "nan", use "NaN" instead') try: # Assume Python compatible syntax. return float(value) except ValueError: # Check alternative spellings. if value == _NEG_INFINITY: return float('-inf') elif value == _INFINITY: return float('inf') elif value == _NAN: return float('nan') else: raise ParseError('Couldn\'t parse float: {0}'.format(value)) def _ConvertBool(value, require_str): """Convert a boolean value. Args: value: A scalar value to convert. require_str: If True, value must be a str. Returns: The bool parsed. Raises: ParseError: If a boolean value couldn't be consumed. """ if require_str: if value == 'true': return True elif value == 'false': return False else: raise ParseError('Expected "true" or "false", not {0}'.format(value)) if not isinstance(value, bool): raise ParseError('Expected true or false without quotes') return value _WKTJSONMETHODS = { 'google.protobuf.Any': ['_AnyMessageToJsonObject', '_ConvertAnyMessage'], 'google.protobuf.Duration': ['_GenericMessageToJsonObject', '_ConvertGenericMessage'], 'google.protobuf.FieldMask': ['_GenericMessageToJsonObject', '_ConvertGenericMessage'], 'google.protobuf.ListValue': ['_ListValueMessageToJsonObject', '_ConvertListValueMessage'], 'google.protobuf.Struct': ['_StructMessageToJsonObject', '_ConvertStructMessage'], 'google.protobuf.Timestamp': ['_GenericMessageToJsonObject', '_ConvertGenericMessage'], 'google.protobuf.Value': ['_ValueMessageToJsonObject', '_ConvertValueMessage'] } ================================================ FILE: openpype/hosts/nuke/vendor/google/protobuf/message.py ================================================ # Protocol Buffers - Google's data interchange format # Copyright 2008 Google Inc. All rights reserved. # https://developers.google.com/protocol-buffers/ # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are # met: # # * Redistributions of source code must retain the above copyright # notice, this list of conditions and the following disclaimer. # * Redistributions in binary form must reproduce the above # copyright notice, this list of conditions and the following disclaimer # in the documentation and/or other materials provided with the # distribution. # * Neither the name of Google Inc. nor the names of its # contributors may be used to endorse or promote products derived from # this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. # TODO(robinson): We should just make these methods all "pure-virtual" and move # all implementation out, into reflection.py for now. """Contains an abstract base class for protocol messages.""" __author__ = 'robinson@google.com (Will Robinson)' class Error(Exception): """Base error type for this module.""" pass class DecodeError(Error): """Exception raised when deserializing messages.""" pass class EncodeError(Error): """Exception raised when serializing messages.""" pass class Message(object): """Abstract base class for protocol messages. Protocol message classes are almost always generated by the protocol compiler. These generated types subclass Message and implement the methods shown below. """ # TODO(robinson): Link to an HTML document here. # TODO(robinson): Document that instances of this class will also # have an Extensions attribute with __getitem__ and __setitem__. # Again, not sure how to best convey this. # TODO(robinson): Document that the class must also have a static # RegisterExtension(extension_field) method. # Not sure how to best express at this point. # TODO(robinson): Document these fields and methods. __slots__ = [] #: The :class:`google.protobuf.descriptor.Descriptor` for this message type. DESCRIPTOR = None def __deepcopy__(self, memo=None): clone = type(self)() clone.MergeFrom(self) return clone def __eq__(self, other_msg): """Recursively compares two messages by value and structure.""" raise NotImplementedError def __ne__(self, other_msg): # Can't just say self != other_msg, since that would infinitely recurse. :) return not self == other_msg def __hash__(self): raise TypeError('unhashable object') def __str__(self): """Outputs a human-readable representation of the message.""" raise NotImplementedError def __unicode__(self): """Outputs a human-readable representation of the message.""" raise NotImplementedError def MergeFrom(self, other_msg): """Merges the contents of the specified message into current message. This method merges the contents of the specified message into the current message. Singular fields that are set in the specified message overwrite the corresponding fields in the current message. Repeated fields are appended. Singular sub-messages and groups are recursively merged. Args: other_msg (Message): A message to merge into the current message. """ raise NotImplementedError def CopyFrom(self, other_msg): """Copies the content of the specified message into the current message. The method clears the current message and then merges the specified message using MergeFrom. Args: other_msg (Message): A message to copy into the current one. """ if self is other_msg: return self.Clear() self.MergeFrom(other_msg) def Clear(self): """Clears all data that was set in the message.""" raise NotImplementedError def SetInParent(self): """Mark this as present in the parent. This normally happens automatically when you assign a field of a sub-message, but sometimes you want to make the sub-message present while keeping it empty. If you find yourself using this, you may want to reconsider your design. """ raise NotImplementedError def IsInitialized(self): """Checks if the message is initialized. Returns: bool: The method returns True if the message is initialized (i.e. all of its required fields are set). """ raise NotImplementedError # TODO(robinson): MergeFromString() should probably return None and be # implemented in terms of a helper that returns the # of bytes read. Our # deserialization routines would use the helper when recursively # deserializing, but the end user would almost always just want the no-return # MergeFromString(). def MergeFromString(self, serialized): """Merges serialized protocol buffer data into this message. When we find a field in `serialized` that is already present in this message: - If it's a "repeated" field, we append to the end of our list. - Else, if it's a scalar, we overwrite our field. - Else, (it's a nonrepeated composite), we recursively merge into the existing composite. Args: serialized (bytes): Any object that allows us to call ``memoryview(serialized)`` to access a string of bytes using the buffer interface. Returns: int: The number of bytes read from `serialized`. For non-group messages, this will always be `len(serialized)`, but for messages which are actually groups, this will generally be less than `len(serialized)`, since we must stop when we reach an ``END_GROUP`` tag. Note that if we *do* stop because of an ``END_GROUP`` tag, the number of bytes returned does not include the bytes for the ``END_GROUP`` tag information. Raises: DecodeError: if the input cannot be parsed. """ # TODO(robinson): Document handling of unknown fields. # TODO(robinson): When we switch to a helper, this will return None. raise NotImplementedError def ParseFromString(self, serialized): """Parse serialized protocol buffer data into this message. Like :func:`MergeFromString()`, except we clear the object first. Raises: message.DecodeError if the input cannot be parsed. """ self.Clear() return self.MergeFromString(serialized) def SerializeToString(self, **kwargs): """Serializes the protocol message to a binary string. Keyword Args: deterministic (bool): If true, requests deterministic serialization of the protobuf, with predictable ordering of map keys. Returns: A binary string representation of the message if all of the required fields in the message are set (i.e. the message is initialized). Raises: EncodeError: if the message isn't initialized (see :func:`IsInitialized`). """ raise NotImplementedError def SerializePartialToString(self, **kwargs): """Serializes the protocol message to a binary string. This method is similar to SerializeToString but doesn't check if the message is initialized. Keyword Args: deterministic (bool): If true, requests deterministic serialization of the protobuf, with predictable ordering of map keys. Returns: bytes: A serialized representation of the partial message. """ raise NotImplementedError # TODO(robinson): Decide whether we like these better # than auto-generated has_foo() and clear_foo() methods # on the instances themselves. This way is less consistent # with C++, but it makes reflection-type access easier and # reduces the number of magically autogenerated things. # # TODO(robinson): Be sure to document (and test) exactly # which field names are accepted here. Are we case-sensitive? # What do we do with fields that share names with Python keywords # like 'lambda' and 'yield'? # # nnorwitz says: # """ # Typically (in python), an underscore is appended to names that are # keywords. So they would become lambda_ or yield_. # """ def ListFields(self): """Returns a list of (FieldDescriptor, value) tuples for present fields. A message field is non-empty if HasField() would return true. A singular primitive field is non-empty if HasField() would return true in proto2 or it is non zero in proto3. A repeated field is non-empty if it contains at least one element. The fields are ordered by field number. Returns: list[tuple(FieldDescriptor, value)]: field descriptors and values for all fields in the message which are not empty. The values vary by field type. """ raise NotImplementedError def HasField(self, field_name): """Checks if a certain field is set for the message. For a oneof group, checks if any field inside is set. Note that if the field_name is not defined in the message descriptor, :exc:`ValueError` will be raised. Args: field_name (str): The name of the field to check for presence. Returns: bool: Whether a value has been set for the named field. Raises: ValueError: if the `field_name` is not a member of this message. """ raise NotImplementedError def ClearField(self, field_name): """Clears the contents of a given field. Inside a oneof group, clears the field set. If the name neither refers to a defined field or oneof group, :exc:`ValueError` is raised. Args: field_name (str): The name of the field to check for presence. Raises: ValueError: if the `field_name` is not a member of this message. """ raise NotImplementedError def WhichOneof(self, oneof_group): """Returns the name of the field that is set inside a oneof group. If no field is set, returns None. Args: oneof_group (str): the name of the oneof group to check. Returns: str or None: The name of the group that is set, or None. Raises: ValueError: no group with the given name exists """ raise NotImplementedError def HasExtension(self, extension_handle): """Checks if a certain extension is present for this message. Extensions are retrieved using the :attr:`Extensions` mapping (if present). Args: extension_handle: The handle for the extension to check. Returns: bool: Whether the extension is present for this message. Raises: KeyError: if the extension is repeated. Similar to repeated fields, there is no separate notion of presence: a "not present" repeated extension is an empty list. """ raise NotImplementedError def ClearExtension(self, extension_handle): """Clears the contents of a given extension. Args: extension_handle: The handle for the extension to clear. """ raise NotImplementedError def UnknownFields(self): """Returns the UnknownFieldSet. Returns: UnknownFieldSet: The unknown fields stored in this message. """ raise NotImplementedError def DiscardUnknownFields(self): """Clears all fields in the :class:`UnknownFieldSet`. This operation is recursive for nested message. """ raise NotImplementedError def ByteSize(self): """Returns the serialized size of this message. Recursively calls ByteSize() on all contained messages. Returns: int: The number of bytes required to serialize this message. """ raise NotImplementedError @classmethod def FromString(cls, s): raise NotImplementedError @staticmethod def RegisterExtension(extension_handle): raise NotImplementedError def _SetListener(self, message_listener): """Internal method used by the protocol message implementation. Clients should not call this directly. Sets a listener that this message will call on certain state transitions. The purpose of this method is to register back-edges from children to parents at runtime, for the purpose of setting "has" bits and byte-size-dirty bits in the parent and ancestor objects whenever a child or descendant object is modified. If the client wants to disconnect this Message from the object tree, she explicitly sets callback to None. If message_listener is None, unregisters any existing listener. Otherwise, message_listener must implement the MessageListener interface in internal/message_listener.py, and we discard any listener registered via a previous _SetListener() call. """ raise NotImplementedError def __getstate__(self): """Support the pickle protocol.""" return dict(serialized=self.SerializePartialToString()) def __setstate__(self, state): """Support the pickle protocol.""" self.__init__() serialized = state['serialized'] # On Python 3, using encoding='latin1' is required for unpickling # protos pickled by Python 2. if not isinstance(serialized, bytes): serialized = serialized.encode('latin1') self.ParseFromString(serialized) def __reduce__(self): message_descriptor = self.DESCRIPTOR if message_descriptor.containing_type is None: return type(self), (), self.__getstate__() # the message type must be nested. # Python does not pickle nested classes; use the symbol_database on the # receiving end. container = message_descriptor return (_InternalConstructMessage, (container.full_name,), self.__getstate__()) def _InternalConstructMessage(full_name): """Constructs a nested message.""" from google.protobuf import symbol_database # pylint:disable=g-import-not-at-top return symbol_database.Default().GetSymbol(full_name)() ================================================ FILE: openpype/hosts/nuke/vendor/google/protobuf/message_factory.py ================================================ # Protocol Buffers - Google's data interchange format # Copyright 2008 Google Inc. All rights reserved. # https://developers.google.com/protocol-buffers/ # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are # met: # # * Redistributions of source code must retain the above copyright # notice, this list of conditions and the following disclaimer. # * Redistributions in binary form must reproduce the above # copyright notice, this list of conditions and the following disclaimer # in the documentation and/or other materials provided with the # distribution. # * Neither the name of Google Inc. nor the names of its # contributors may be used to endorse or promote products derived from # this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. """Provides a factory class for generating dynamic messages. The easiest way to use this class is if you have access to the FileDescriptor protos containing the messages you want to create you can just do the following: message_classes = message_factory.GetMessages(iterable_of_file_descriptors) my_proto_instance = message_classes['some.proto.package.MessageName']() """ __author__ = 'matthewtoia@google.com (Matt Toia)' from google.protobuf.internal import api_implementation from google.protobuf import descriptor_pool from google.protobuf import message if api_implementation.Type() == 'cpp': from google.protobuf.pyext import cpp_message as message_impl else: from google.protobuf.internal import python_message as message_impl # The type of all Message classes. _GENERATED_PROTOCOL_MESSAGE_TYPE = message_impl.GeneratedProtocolMessageType class MessageFactory(object): """Factory for creating Proto2 messages from descriptors in a pool.""" def __init__(self, pool=None): """Initializes a new factory.""" self.pool = pool or descriptor_pool.DescriptorPool() # local cache of all classes built from protobuf descriptors self._classes = {} def GetPrototype(self, descriptor): """Obtains a proto2 message class based on the passed in descriptor. Passing a descriptor with a fully qualified name matching a previous invocation will cause the same class to be returned. Args: descriptor: The descriptor to build from. Returns: A class describing the passed in descriptor. """ if descriptor not in self._classes: result_class = self.CreatePrototype(descriptor) # The assignment to _classes is redundant for the base implementation, but # might avoid confusion in cases where CreatePrototype gets overridden and # does not call the base implementation. self._classes[descriptor] = result_class return result_class return self._classes[descriptor] def CreatePrototype(self, descriptor): """Builds a proto2 message class based on the passed in descriptor. Don't call this function directly, it always creates a new class. Call GetPrototype() instead. This method is meant to be overridden in subblasses to perform additional operations on the newly constructed class. Args: descriptor: The descriptor to build from. Returns: A class describing the passed in descriptor. """ descriptor_name = descriptor.name result_class = _GENERATED_PROTOCOL_MESSAGE_TYPE( descriptor_name, (message.Message,), { 'DESCRIPTOR': descriptor, # If module not set, it wrongly points to message_factory module. '__module__': None, }) result_class._FACTORY = self # pylint: disable=protected-access # Assign in _classes before doing recursive calls to avoid infinite # recursion. self._classes[descriptor] = result_class for field in descriptor.fields: if field.message_type: self.GetPrototype(field.message_type) for extension in result_class.DESCRIPTOR.extensions: if extension.containing_type not in self._classes: self.GetPrototype(extension.containing_type) extended_class = self._classes[extension.containing_type] extended_class.RegisterExtension(extension) return result_class def GetMessages(self, files): """Gets all the messages from a specified file. This will find and resolve dependencies, failing if the descriptor pool cannot satisfy them. Args: files: The file names to extract messages from. Returns: A dictionary mapping proto names to the message classes. This will include any dependent messages as well as any messages defined in the same file as a specified message. """ result = {} for file_name in files: file_desc = self.pool.FindFileByName(file_name) for desc in file_desc.message_types_by_name.values(): result[desc.full_name] = self.GetPrototype(desc) # While the extension FieldDescriptors are created by the descriptor pool, # the python classes created in the factory need them to be registered # explicitly, which is done below. # # The call to RegisterExtension will specifically check if the # extension was already registered on the object and either # ignore the registration if the original was the same, or raise # an error if they were different. for extension in file_desc.extensions_by_name.values(): if extension.containing_type not in self._classes: self.GetPrototype(extension.containing_type) extended_class = self._classes[extension.containing_type] extended_class.RegisterExtension(extension) return result _FACTORY = MessageFactory() def GetMessages(file_protos): """Builds a dictionary of all the messages available in a set of files. Args: file_protos: Iterable of FileDescriptorProto to build messages out of. Returns: A dictionary mapping proto names to the message classes. This will include any dependent messages as well as any messages defined in the same file as a specified message. """ # The cpp implementation of the protocol buffer library requires to add the # message in topological order of the dependency graph. file_by_name = {file_proto.name: file_proto for file_proto in file_protos} def _AddFile(file_proto): for dependency in file_proto.dependency: if dependency in file_by_name: # Remove from elements to be visited, in order to cut cycles. _AddFile(file_by_name.pop(dependency)) _FACTORY.pool.Add(file_proto) while file_by_name: _AddFile(file_by_name.popitem()[1]) return _FACTORY.GetMessages([file_proto.name for file_proto in file_protos]) ================================================ FILE: openpype/hosts/nuke/vendor/google/protobuf/proto_builder.py ================================================ # Protocol Buffers - Google's data interchange format # Copyright 2008 Google Inc. All rights reserved. # https://developers.google.com/protocol-buffers/ # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are # met: # # * Redistributions of source code must retain the above copyright # notice, this list of conditions and the following disclaimer. # * Redistributions in binary form must reproduce the above # copyright notice, this list of conditions and the following disclaimer # in the documentation and/or other materials provided with the # distribution. # * Neither the name of Google Inc. nor the names of its # contributors may be used to endorse or promote products derived from # this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. """Dynamic Protobuf class creator.""" from collections import OrderedDict import hashlib import os from google.protobuf import descriptor_pb2 from google.protobuf import descriptor from google.protobuf import message_factory def _GetMessageFromFactory(factory, full_name): """Get a proto class from the MessageFactory by name. Args: factory: a MessageFactory instance. full_name: str, the fully qualified name of the proto type. Returns: A class, for the type identified by full_name. Raises: KeyError, if the proto is not found in the factory's descriptor pool. """ proto_descriptor = factory.pool.FindMessageTypeByName(full_name) proto_cls = factory.GetPrototype(proto_descriptor) return proto_cls def MakeSimpleProtoClass(fields, full_name=None, pool=None): """Create a Protobuf class whose fields are basic types. Note: this doesn't validate field names! Args: fields: dict of {name: field_type} mappings for each field in the proto. If this is an OrderedDict the order will be maintained, otherwise the fields will be sorted by name. full_name: optional str, the fully-qualified name of the proto type. pool: optional DescriptorPool instance. Returns: a class, the new protobuf class with a FileDescriptor. """ factory = message_factory.MessageFactory(pool=pool) if full_name is not None: try: proto_cls = _GetMessageFromFactory(factory, full_name) return proto_cls except KeyError: # The factory's DescriptorPool doesn't know about this class yet. pass # Get a list of (name, field_type) tuples from the fields dict. If fields was # an OrderedDict we keep the order, but otherwise we sort the field to ensure # consistent ordering. field_items = fields.items() if not isinstance(fields, OrderedDict): field_items = sorted(field_items) # Use a consistent file name that is unlikely to conflict with any imported # proto files. fields_hash = hashlib.sha1() for f_name, f_type in field_items: fields_hash.update(f_name.encode('utf-8')) fields_hash.update(str(f_type).encode('utf-8')) proto_file_name = fields_hash.hexdigest() + '.proto' # If the proto is anonymous, use the same hash to name it. if full_name is None: full_name = ('net.proto2.python.public.proto_builder.AnonymousProto_' + fields_hash.hexdigest()) try: proto_cls = _GetMessageFromFactory(factory, full_name) return proto_cls except KeyError: # The factory's DescriptorPool doesn't know about this class yet. pass # This is the first time we see this proto: add a new descriptor to the pool. factory.pool.Add( _MakeFileDescriptorProto(proto_file_name, full_name, field_items)) return _GetMessageFromFactory(factory, full_name) def _MakeFileDescriptorProto(proto_file_name, full_name, field_items): """Populate FileDescriptorProto for MessageFactory's DescriptorPool.""" package, name = full_name.rsplit('.', 1) file_proto = descriptor_pb2.FileDescriptorProto() file_proto.name = os.path.join(package.replace('.', '/'), proto_file_name) file_proto.package = package desc_proto = file_proto.message_type.add() desc_proto.name = name for f_number, (f_name, f_type) in enumerate(field_items, 1): field_proto = desc_proto.field.add() field_proto.name = f_name # # If the number falls in the reserved range, reassign it to the correct # # number after the range. if f_number >= descriptor.FieldDescriptor.FIRST_RESERVED_FIELD_NUMBER: f_number += ( descriptor.FieldDescriptor.LAST_RESERVED_FIELD_NUMBER - descriptor.FieldDescriptor.FIRST_RESERVED_FIELD_NUMBER + 1) field_proto.number = f_number field_proto.label = descriptor_pb2.FieldDescriptorProto.LABEL_OPTIONAL field_proto.type = f_type return file_proto ================================================ FILE: openpype/hosts/nuke/vendor/google/protobuf/pyext/__init__.py ================================================ ================================================ FILE: openpype/hosts/nuke/vendor/google/protobuf/pyext/cpp_message.py ================================================ # Protocol Buffers - Google's data interchange format # Copyright 2008 Google Inc. All rights reserved. # https://developers.google.com/protocol-buffers/ # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are # met: # # * Redistributions of source code must retain the above copyright # notice, this list of conditions and the following disclaimer. # * Redistributions in binary form must reproduce the above # copyright notice, this list of conditions and the following disclaimer # in the documentation and/or other materials provided with the # distribution. # * Neither the name of Google Inc. nor the names of its # contributors may be used to endorse or promote products derived from # this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. """Protocol message implementation hooks for C++ implementation. Contains helper functions used to create protocol message classes from Descriptor objects at runtime backed by the protocol buffer C++ API. """ __author__ = 'tibell@google.com (Johan Tibell)' from google.protobuf.pyext import _message class GeneratedProtocolMessageType(_message.MessageMeta): """Metaclass for protocol message classes created at runtime from Descriptors. The protocol compiler currently uses this metaclass to create protocol message classes at runtime. Clients can also manually create their own classes at runtime, as in this example: mydescriptor = Descriptor(.....) factory = symbol_database.Default() factory.pool.AddDescriptor(mydescriptor) MyProtoClass = factory.GetPrototype(mydescriptor) myproto_instance = MyProtoClass() myproto.foo_field = 23 ... The above example will not work for nested types. If you wish to include them, use reflection.MakeClass() instead of manually instantiating the class in order to create the appropriate class structure. """ # Must be consistent with the protocol-compiler code in # proto2/compiler/internal/generator.*. _DESCRIPTOR_KEY = 'DESCRIPTOR' ================================================ FILE: openpype/hosts/nuke/vendor/google/protobuf/pyext/python_pb2.py ================================================ # -*- coding: utf-8 -*- # Generated by the protocol buffer compiler. DO NOT EDIT! # source: google/protobuf/pyext/python.proto """Generated protocol buffer code.""" from google.protobuf.internal import builder as _builder from google.protobuf import descriptor as _descriptor from google.protobuf import descriptor_pool as _descriptor_pool from google.protobuf import symbol_database as _symbol_database # @@protoc_insertion_point(imports) _sym_db = _symbol_database.Default() DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\"google/protobuf/pyext/python.proto\x12\x1fgoogle.protobuf.python.internal\"\xbc\x02\n\x0cTestAllTypes\x12\\\n\x17repeated_nested_message\x18\x01 \x03(\x0b\x32;.google.protobuf.python.internal.TestAllTypes.NestedMessage\x12\\\n\x17optional_nested_message\x18\x02 \x01(\x0b\x32;.google.protobuf.python.internal.TestAllTypes.NestedMessage\x12\x16\n\x0eoptional_int32\x18\x03 \x01(\x05\x1aX\n\rNestedMessage\x12\n\n\x02\x62\x62\x18\x01 \x01(\x05\x12;\n\x02\x63\x63\x18\x02 \x01(\x0b\x32/.google.protobuf.python.internal.ForeignMessage\"&\n\x0e\x46oreignMessage\x12\t\n\x01\x63\x18\x01 \x01(\x05\x12\t\n\x01\x64\x18\x02 \x03(\x05\"\x1d\n\x11TestAllExtensions*\x08\x08\x01\x10\x80\x80\x80\x80\x02:\x9a\x01\n!optional_nested_message_extension\x12\x32.google.protobuf.python.internal.TestAllExtensions\x18\x01 \x01(\x0b\x32;.google.protobuf.python.internal.TestAllTypes.NestedMessage:\x9a\x01\n!repeated_nested_message_extension\x12\x32.google.protobuf.python.internal.TestAllExtensions\x18\x02 \x03(\x0b\x32;.google.protobuf.python.internal.TestAllTypes.NestedMessageB\x02H\x01') _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals()) _builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'google.protobuf.pyext.python_pb2', globals()) if _descriptor._USE_C_DESCRIPTORS == False: TestAllExtensions.RegisterExtension(optional_nested_message_extension) TestAllExtensions.RegisterExtension(repeated_nested_message_extension) DESCRIPTOR._options = None DESCRIPTOR._serialized_options = b'H\001' _TESTALLTYPES._serialized_start=72 _TESTALLTYPES._serialized_end=388 _TESTALLTYPES_NESTEDMESSAGE._serialized_start=300 _TESTALLTYPES_NESTEDMESSAGE._serialized_end=388 _FOREIGNMESSAGE._serialized_start=390 _FOREIGNMESSAGE._serialized_end=428 _TESTALLEXTENSIONS._serialized_start=430 _TESTALLEXTENSIONS._serialized_end=459 # @@protoc_insertion_point(module_scope) ================================================ FILE: openpype/hosts/nuke/vendor/google/protobuf/reflection.py ================================================ # Protocol Buffers - Google's data interchange format # Copyright 2008 Google Inc. All rights reserved. # https://developers.google.com/protocol-buffers/ # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are # met: # # * Redistributions of source code must retain the above copyright # notice, this list of conditions and the following disclaimer. # * Redistributions in binary form must reproduce the above # copyright notice, this list of conditions and the following disclaimer # in the documentation and/or other materials provided with the # distribution. # * Neither the name of Google Inc. nor the names of its # contributors may be used to endorse or promote products derived from # this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. # This code is meant to work on Python 2.4 and above only. """Contains a metaclass and helper functions used to create protocol message classes from Descriptor objects at runtime. Recall that a metaclass is the "type" of a class. (A class is to a metaclass what an instance is to a class.) In this case, we use the GeneratedProtocolMessageType metaclass to inject all the useful functionality into the classes output by the protocol compiler at compile-time. The upshot of all this is that the real implementation details for ALL pure-Python protocol buffers are *here in this file*. """ __author__ = 'robinson@google.com (Will Robinson)' from google.protobuf import message_factory from google.protobuf import symbol_database # The type of all Message classes. # Part of the public interface, but normally only used by message factories. GeneratedProtocolMessageType = message_factory._GENERATED_PROTOCOL_MESSAGE_TYPE MESSAGE_CLASS_CACHE = {} # Deprecated. Please NEVER use reflection.ParseMessage(). def ParseMessage(descriptor, byte_str): """Generate a new Message instance from this Descriptor and a byte string. DEPRECATED: ParseMessage is deprecated because it is using MakeClass(). Please use MessageFactory.GetPrototype() instead. Args: descriptor: Protobuf Descriptor object byte_str: Serialized protocol buffer byte string Returns: Newly created protobuf Message object. """ result_class = MakeClass(descriptor) new_msg = result_class() new_msg.ParseFromString(byte_str) return new_msg # Deprecated. Please NEVER use reflection.MakeClass(). def MakeClass(descriptor): """Construct a class object for a protobuf described by descriptor. DEPRECATED: use MessageFactory.GetPrototype() instead. Args: descriptor: A descriptor.Descriptor object describing the protobuf. Returns: The Message class object described by the descriptor. """ # Original implementation leads to duplicate message classes, which won't play # well with extensions. Message factory info is also missing. # Redirect to message_factory. return symbol_database.Default().GetPrototype(descriptor) ================================================ FILE: openpype/hosts/nuke/vendor/google/protobuf/service.py ================================================ # Protocol Buffers - Google's data interchange format # Copyright 2008 Google Inc. All rights reserved. # https://developers.google.com/protocol-buffers/ # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are # met: # # * Redistributions of source code must retain the above copyright # notice, this list of conditions and the following disclaimer. # * Redistributions in binary form must reproduce the above # copyright notice, this list of conditions and the following disclaimer # in the documentation and/or other materials provided with the # distribution. # * Neither the name of Google Inc. nor the names of its # contributors may be used to endorse or promote products derived from # this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. """DEPRECATED: Declares the RPC service interfaces. This module declares the abstract interfaces underlying proto2 RPC services. These are intended to be independent of any particular RPC implementation, so that proto2 services can be used on top of a variety of implementations. Starting with version 2.3.0, RPC implementations should not try to build on these, but should instead provide code generator plugins which generate code specific to the particular RPC implementation. This way the generated code can be more appropriate for the implementation in use and can avoid unnecessary layers of indirection. """ __author__ = 'petar@google.com (Petar Petrov)' class RpcException(Exception): """Exception raised on failed blocking RPC method call.""" pass class Service(object): """Abstract base interface for protocol-buffer-based RPC services. Services themselves are abstract classes (implemented either by servers or as stubs), but they subclass this base interface. The methods of this interface can be used to call the methods of the service without knowing its exact type at compile time (analogous to the Message interface). """ def GetDescriptor(): """Retrieves this service's descriptor.""" raise NotImplementedError def CallMethod(self, method_descriptor, rpc_controller, request, done): """Calls a method of the service specified by method_descriptor. If "done" is None then the call is blocking and the response message will be returned directly. Otherwise the call is asynchronous and "done" will later be called with the response value. In the blocking case, RpcException will be raised on error. Preconditions: * method_descriptor.service == GetDescriptor * request is of the exact same classes as returned by GetRequestClass(method). * After the call has started, the request must not be modified. * "rpc_controller" is of the correct type for the RPC implementation being used by this Service. For stubs, the "correct type" depends on the RpcChannel which the stub is using. Postconditions: * "done" will be called when the method is complete. This may be before CallMethod() returns or it may be at some point in the future. * If the RPC failed, the response value passed to "done" will be None. Further details about the failure can be found by querying the RpcController. """ raise NotImplementedError def GetRequestClass(self, method_descriptor): """Returns the class of the request message for the specified method. CallMethod() requires that the request is of a particular subclass of Message. GetRequestClass() gets the default instance of this required type. Example: method = service.GetDescriptor().FindMethodByName("Foo") request = stub.GetRequestClass(method)() request.ParseFromString(input) service.CallMethod(method, request, callback) """ raise NotImplementedError def GetResponseClass(self, method_descriptor): """Returns the class of the response message for the specified method. This method isn't really needed, as the RpcChannel's CallMethod constructs the response protocol message. It's provided anyway in case it is useful for the caller to know the response type in advance. """ raise NotImplementedError class RpcController(object): """An RpcController mediates a single method call. The primary purpose of the controller is to provide a way to manipulate settings specific to the RPC implementation and to find out about RPC-level errors. The methods provided by the RpcController interface are intended to be a "least common denominator" set of features which we expect all implementations to support. Specific implementations may provide more advanced features (e.g. deadline propagation). """ # Client-side methods below def Reset(self): """Resets the RpcController to its initial state. After the RpcController has been reset, it may be reused in a new call. Must not be called while an RPC is in progress. """ raise NotImplementedError def Failed(self): """Returns true if the call failed. After a call has finished, returns true if the call failed. The possible reasons for failure depend on the RPC implementation. Failed() must not be called before a call has finished. If Failed() returns true, the contents of the response message are undefined. """ raise NotImplementedError def ErrorText(self): """If Failed is true, returns a human-readable description of the error.""" raise NotImplementedError def StartCancel(self): """Initiate cancellation. Advises the RPC system that the caller desires that the RPC call be canceled. The RPC system may cancel it immediately, may wait awhile and then cancel it, or may not even cancel the call at all. If the call is canceled, the "done" callback will still be called and the RpcController will indicate that the call failed at that time. """ raise NotImplementedError # Server-side methods below def SetFailed(self, reason): """Sets a failure reason. Causes Failed() to return true on the client side. "reason" will be incorporated into the message returned by ErrorText(). If you find you need to return machine-readable information about failures, you should incorporate it into your response protocol buffer and should NOT call SetFailed(). """ raise NotImplementedError def IsCanceled(self): """Checks if the client cancelled the RPC. If true, indicates that the client canceled the RPC, so the server may as well give up on replying to it. The server should still call the final "done" callback. """ raise NotImplementedError def NotifyOnCancel(self, callback): """Sets a callback to invoke on cancel. Asks that the given callback be called when the RPC is canceled. The callback will always be called exactly once. If the RPC completes without being canceled, the callback will be called after completion. If the RPC has already been canceled when NotifyOnCancel() is called, the callback will be called immediately. NotifyOnCancel() must be called no more than once per request. """ raise NotImplementedError class RpcChannel(object): """Abstract interface for an RPC channel. An RpcChannel represents a communication line to a service which can be used to call that service's methods. The service may be running on another machine. Normally, you should not use an RpcChannel directly, but instead construct a stub {@link Service} wrapping it. Example: Example: RpcChannel channel = rpcImpl.Channel("remotehost.example.com:1234") RpcController controller = rpcImpl.Controller() MyService service = MyService_Stub(channel) service.MyMethod(controller, request, callback) """ def CallMethod(self, method_descriptor, rpc_controller, request, response_class, done): """Calls the method identified by the descriptor. Call the given method of the remote service. The signature of this procedure looks the same as Service.CallMethod(), but the requirements are less strict in one important way: the request object doesn't have to be of any specific class as long as its descriptor is method.input_type. """ raise NotImplementedError ================================================ FILE: openpype/hosts/nuke/vendor/google/protobuf/service_reflection.py ================================================ # Protocol Buffers - Google's data interchange format # Copyright 2008 Google Inc. All rights reserved. # https://developers.google.com/protocol-buffers/ # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are # met: # # * Redistributions of source code must retain the above copyright # notice, this list of conditions and the following disclaimer. # * Redistributions in binary form must reproduce the above # copyright notice, this list of conditions and the following disclaimer # in the documentation and/or other materials provided with the # distribution. # * Neither the name of Google Inc. nor the names of its # contributors may be used to endorse or promote products derived from # this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. """Contains metaclasses used to create protocol service and service stub classes from ServiceDescriptor objects at runtime. The GeneratedServiceType and GeneratedServiceStubType metaclasses are used to inject all useful functionality into the classes output by the protocol compiler at compile-time. """ __author__ = 'petar@google.com (Petar Petrov)' class GeneratedServiceType(type): """Metaclass for service classes created at runtime from ServiceDescriptors. Implementations for all methods described in the Service class are added here by this class. We also create properties to allow getting/setting all fields in the protocol message. The protocol compiler currently uses this metaclass to create protocol service classes at runtime. Clients can also manually create their own classes at runtime, as in this example:: mydescriptor = ServiceDescriptor(.....) class MyProtoService(service.Service): __metaclass__ = GeneratedServiceType DESCRIPTOR = mydescriptor myservice_instance = MyProtoService() # ... """ _DESCRIPTOR_KEY = 'DESCRIPTOR' def __init__(cls, name, bases, dictionary): """Creates a message service class. Args: name: Name of the class (ignored, but required by the metaclass protocol). bases: Base classes of the class being constructed. dictionary: The class dictionary of the class being constructed. dictionary[_DESCRIPTOR_KEY] must contain a ServiceDescriptor object describing this protocol service type. """ # Don't do anything if this class doesn't have a descriptor. This happens # when a service class is subclassed. if GeneratedServiceType._DESCRIPTOR_KEY not in dictionary: return descriptor = dictionary[GeneratedServiceType._DESCRIPTOR_KEY] service_builder = _ServiceBuilder(descriptor) service_builder.BuildService(cls) cls.DESCRIPTOR = descriptor class GeneratedServiceStubType(GeneratedServiceType): """Metaclass for service stubs created at runtime from ServiceDescriptors. This class has similar responsibilities as GeneratedServiceType, except that it creates the service stub classes. """ _DESCRIPTOR_KEY = 'DESCRIPTOR' def __init__(cls, name, bases, dictionary): """Creates a message service stub class. Args: name: Name of the class (ignored, here). bases: Base classes of the class being constructed. dictionary: The class dictionary of the class being constructed. dictionary[_DESCRIPTOR_KEY] must contain a ServiceDescriptor object describing this protocol service type. """ super(GeneratedServiceStubType, cls).__init__(name, bases, dictionary) # Don't do anything if this class doesn't have a descriptor. This happens # when a service stub is subclassed. if GeneratedServiceStubType._DESCRIPTOR_KEY not in dictionary: return descriptor = dictionary[GeneratedServiceStubType._DESCRIPTOR_KEY] service_stub_builder = _ServiceStubBuilder(descriptor) service_stub_builder.BuildServiceStub(cls) class _ServiceBuilder(object): """This class constructs a protocol service class using a service descriptor. Given a service descriptor, this class constructs a class that represents the specified service descriptor. One service builder instance constructs exactly one service class. That means all instances of that class share the same builder. """ def __init__(self, service_descriptor): """Initializes an instance of the service class builder. Args: service_descriptor: ServiceDescriptor to use when constructing the service class. """ self.descriptor = service_descriptor def BuildService(builder, cls): """Constructs the service class. Args: cls: The class that will be constructed. """ # CallMethod needs to operate with an instance of the Service class. This # internal wrapper function exists only to be able to pass the service # instance to the method that does the real CallMethod work. # Making sure to use exact argument names from the abstract interface in # service.py to match the type signature def _WrapCallMethod(self, method_descriptor, rpc_controller, request, done): return builder._CallMethod(self, method_descriptor, rpc_controller, request, done) def _WrapGetRequestClass(self, method_descriptor): return builder._GetRequestClass(method_descriptor) def _WrapGetResponseClass(self, method_descriptor): return builder._GetResponseClass(method_descriptor) builder.cls = cls cls.CallMethod = _WrapCallMethod cls.GetDescriptor = staticmethod(lambda: builder.descriptor) cls.GetDescriptor.__doc__ = 'Returns the service descriptor.' cls.GetRequestClass = _WrapGetRequestClass cls.GetResponseClass = _WrapGetResponseClass for method in builder.descriptor.methods: setattr(cls, method.name, builder._GenerateNonImplementedMethod(method)) def _CallMethod(self, srvc, method_descriptor, rpc_controller, request, callback): """Calls the method described by a given method descriptor. Args: srvc: Instance of the service for which this method is called. method_descriptor: Descriptor that represent the method to call. rpc_controller: RPC controller to use for this method's execution. request: Request protocol message. callback: A callback to invoke after the method has completed. """ if method_descriptor.containing_service != self.descriptor: raise RuntimeError( 'CallMethod() given method descriptor for wrong service type.') method = getattr(srvc, method_descriptor.name) return method(rpc_controller, request, callback) def _GetRequestClass(self, method_descriptor): """Returns the class of the request protocol message. Args: method_descriptor: Descriptor of the method for which to return the request protocol message class. Returns: A class that represents the input protocol message of the specified method. """ if method_descriptor.containing_service != self.descriptor: raise RuntimeError( 'GetRequestClass() given method descriptor for wrong service type.') return method_descriptor.input_type._concrete_class def _GetResponseClass(self, method_descriptor): """Returns the class of the response protocol message. Args: method_descriptor: Descriptor of the method for which to return the response protocol message class. Returns: A class that represents the output protocol message of the specified method. """ if method_descriptor.containing_service != self.descriptor: raise RuntimeError( 'GetResponseClass() given method descriptor for wrong service type.') return method_descriptor.output_type._concrete_class def _GenerateNonImplementedMethod(self, method): """Generates and returns a method that can be set for a service methods. Args: method: Descriptor of the service method for which a method is to be generated. Returns: A method that can be added to the service class. """ return lambda inst, rpc_controller, request, callback: ( self._NonImplementedMethod(method.name, rpc_controller, callback)) def _NonImplementedMethod(self, method_name, rpc_controller, callback): """The body of all methods in the generated service class. Args: method_name: Name of the method being executed. rpc_controller: RPC controller used to execute this method. callback: A callback which will be invoked when the method finishes. """ rpc_controller.SetFailed('Method %s not implemented.' % method_name) callback(None) class _ServiceStubBuilder(object): """Constructs a protocol service stub class using a service descriptor. Given a service descriptor, this class constructs a suitable stub class. A stub is just a type-safe wrapper around an RpcChannel which emulates a local implementation of the service. One service stub builder instance constructs exactly one class. It means all instances of that class share the same service stub builder. """ def __init__(self, service_descriptor): """Initializes an instance of the service stub class builder. Args: service_descriptor: ServiceDescriptor to use when constructing the stub class. """ self.descriptor = service_descriptor def BuildServiceStub(self, cls): """Constructs the stub class. Args: cls: The class that will be constructed. """ def _ServiceStubInit(stub, rpc_channel): stub.rpc_channel = rpc_channel self.cls = cls cls.__init__ = _ServiceStubInit for method in self.descriptor.methods: setattr(cls, method.name, self._GenerateStubMethod(method)) def _GenerateStubMethod(self, method): return (lambda inst, rpc_controller, request, callback=None: self._StubMethod(inst, method, rpc_controller, request, callback)) def _StubMethod(self, stub, method_descriptor, rpc_controller, request, callback): """The body of all service methods in the generated stub class. Args: stub: Stub instance. method_descriptor: Descriptor of the invoked method. rpc_controller: Rpc controller to execute the method. request: Request protocol message. callback: A callback to execute when the method finishes. Returns: Response message (in case of blocking call). """ return stub.rpc_channel.CallMethod( method_descriptor, rpc_controller, request, method_descriptor.output_type._concrete_class, callback) ================================================ FILE: openpype/hosts/nuke/vendor/google/protobuf/source_context_pb2.py ================================================ # -*- coding: utf-8 -*- # Generated by the protocol buffer compiler. DO NOT EDIT! # source: google/protobuf/source_context.proto """Generated protocol buffer code.""" from google.protobuf.internal import builder as _builder from google.protobuf import descriptor as _descriptor from google.protobuf import descriptor_pool as _descriptor_pool from google.protobuf import symbol_database as _symbol_database # @@protoc_insertion_point(imports) _sym_db = _symbol_database.Default() DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n$google/protobuf/source_context.proto\x12\x0fgoogle.protobuf\"\"\n\rSourceContext\x12\x11\n\tfile_name\x18\x01 \x01(\tB\x8a\x01\n\x13\x63om.google.protobufB\x12SourceContextProtoP\x01Z6google.golang.org/protobuf/types/known/sourcecontextpb\xa2\x02\x03GPB\xaa\x02\x1eGoogle.Protobuf.WellKnownTypesb\x06proto3') _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals()) _builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'google.protobuf.source_context_pb2', globals()) if _descriptor._USE_C_DESCRIPTORS == False: DESCRIPTOR._options = None DESCRIPTOR._serialized_options = b'\n\023com.google.protobufB\022SourceContextProtoP\001Z6google.golang.org/protobuf/types/known/sourcecontextpb\242\002\003GPB\252\002\036Google.Protobuf.WellKnownTypes' _SOURCECONTEXT._serialized_start=57 _SOURCECONTEXT._serialized_end=91 # @@protoc_insertion_point(module_scope) ================================================ FILE: openpype/hosts/nuke/vendor/google/protobuf/struct_pb2.py ================================================ # -*- coding: utf-8 -*- # Generated by the protocol buffer compiler. DO NOT EDIT! # source: google/protobuf/struct.proto """Generated protocol buffer code.""" from google.protobuf.internal import builder as _builder from google.protobuf import descriptor as _descriptor from google.protobuf import descriptor_pool as _descriptor_pool from google.protobuf import symbol_database as _symbol_database # @@protoc_insertion_point(imports) _sym_db = _symbol_database.Default() DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x1cgoogle/protobuf/struct.proto\x12\x0fgoogle.protobuf\"\x84\x01\n\x06Struct\x12\x33\n\x06\x66ields\x18\x01 \x03(\x0b\x32#.google.protobuf.Struct.FieldsEntry\x1a\x45\n\x0b\x46ieldsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12%\n\x05value\x18\x02 \x01(\x0b\x32\x16.google.protobuf.Value:\x02\x38\x01\"\xea\x01\n\x05Value\x12\x30\n\nnull_value\x18\x01 \x01(\x0e\x32\x1a.google.protobuf.NullValueH\x00\x12\x16\n\x0cnumber_value\x18\x02 \x01(\x01H\x00\x12\x16\n\x0cstring_value\x18\x03 \x01(\tH\x00\x12\x14\n\nbool_value\x18\x04 \x01(\x08H\x00\x12/\n\x0cstruct_value\x18\x05 \x01(\x0b\x32\x17.google.protobuf.StructH\x00\x12\x30\n\nlist_value\x18\x06 \x01(\x0b\x32\x1a.google.protobuf.ListValueH\x00\x42\x06\n\x04kind\"3\n\tListValue\x12&\n\x06values\x18\x01 \x03(\x0b\x32\x16.google.protobuf.Value*\x1b\n\tNullValue\x12\x0e\n\nNULL_VALUE\x10\x00\x42\x7f\n\x13\x63om.google.protobufB\x0bStructProtoP\x01Z/google.golang.org/protobuf/types/known/structpb\xf8\x01\x01\xa2\x02\x03GPB\xaa\x02\x1eGoogle.Protobuf.WellKnownTypesb\x06proto3') _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals()) _builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'google.protobuf.struct_pb2', globals()) if _descriptor._USE_C_DESCRIPTORS == False: DESCRIPTOR._options = None DESCRIPTOR._serialized_options = b'\n\023com.google.protobufB\013StructProtoP\001Z/google.golang.org/protobuf/types/known/structpb\370\001\001\242\002\003GPB\252\002\036Google.Protobuf.WellKnownTypes' _STRUCT_FIELDSENTRY._options = None _STRUCT_FIELDSENTRY._serialized_options = b'8\001' _NULLVALUE._serialized_start=474 _NULLVALUE._serialized_end=501 _STRUCT._serialized_start=50 _STRUCT._serialized_end=182 _STRUCT_FIELDSENTRY._serialized_start=113 _STRUCT_FIELDSENTRY._serialized_end=182 _VALUE._serialized_start=185 _VALUE._serialized_end=419 _LISTVALUE._serialized_start=421 _LISTVALUE._serialized_end=472 # @@protoc_insertion_point(module_scope) ================================================ FILE: openpype/hosts/nuke/vendor/google/protobuf/symbol_database.py ================================================ # Protocol Buffers - Google's data interchange format # Copyright 2008 Google Inc. All rights reserved. # https://developers.google.com/protocol-buffers/ # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are # met: # # * Redistributions of source code must retain the above copyright # notice, this list of conditions and the following disclaimer. # * Redistributions in binary form must reproduce the above # copyright notice, this list of conditions and the following disclaimer # in the documentation and/or other materials provided with the # distribution. # * Neither the name of Google Inc. nor the names of its # contributors may be used to endorse or promote products derived from # this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. """A database of Python protocol buffer generated symbols. SymbolDatabase is the MessageFactory for messages generated at compile time, and makes it easy to create new instances of a registered type, given only the type's protocol buffer symbol name. Example usage:: db = symbol_database.SymbolDatabase() # Register symbols of interest, from one or multiple files. db.RegisterFileDescriptor(my_proto_pb2.DESCRIPTOR) db.RegisterMessage(my_proto_pb2.MyMessage) db.RegisterEnumDescriptor(my_proto_pb2.MyEnum.DESCRIPTOR) # The database can be used as a MessageFactory, to generate types based on # their name: types = db.GetMessages(['my_proto.proto']) my_message_instance = types['MyMessage']() # The database's underlying descriptor pool can be queried, so it's not # necessary to know a type's filename to be able to generate it: filename = db.pool.FindFileContainingSymbol('MyMessage') my_message_instance = db.GetMessages([filename])['MyMessage']() # This functionality is also provided directly via a convenience method: my_message_instance = db.GetSymbol('MyMessage')() """ from google.protobuf.internal import api_implementation from google.protobuf import descriptor_pool from google.protobuf import message_factory class SymbolDatabase(message_factory.MessageFactory): """A database of Python generated symbols.""" def RegisterMessage(self, message): """Registers the given message type in the local database. Calls to GetSymbol() and GetMessages() will return messages registered here. Args: message: A :class:`google.protobuf.message.Message` subclass (or instance); its descriptor will be registered. Returns: The provided message. """ desc = message.DESCRIPTOR self._classes[desc] = message self.RegisterMessageDescriptor(desc) return message def RegisterMessageDescriptor(self, message_descriptor): """Registers the given message descriptor in the local database. Args: message_descriptor (Descriptor): the message descriptor to add. """ if api_implementation.Type() == 'python': # pylint: disable=protected-access self.pool._AddDescriptor(message_descriptor) def RegisterEnumDescriptor(self, enum_descriptor): """Registers the given enum descriptor in the local database. Args: enum_descriptor (EnumDescriptor): The enum descriptor to register. Returns: EnumDescriptor: The provided descriptor. """ if api_implementation.Type() == 'python': # pylint: disable=protected-access self.pool._AddEnumDescriptor(enum_descriptor) return enum_descriptor def RegisterServiceDescriptor(self, service_descriptor): """Registers the given service descriptor in the local database. Args: service_descriptor (ServiceDescriptor): the service descriptor to register. """ if api_implementation.Type() == 'python': # pylint: disable=protected-access self.pool._AddServiceDescriptor(service_descriptor) def RegisterFileDescriptor(self, file_descriptor): """Registers the given file descriptor in the local database. Args: file_descriptor (FileDescriptor): The file descriptor to register. """ if api_implementation.Type() == 'python': # pylint: disable=protected-access self.pool._InternalAddFileDescriptor(file_descriptor) def GetSymbol(self, symbol): """Tries to find a symbol in the local database. Currently, this method only returns message.Message instances, however, if may be extended in future to support other symbol types. Args: symbol (str): a protocol buffer symbol. Returns: A Python class corresponding to the symbol. Raises: KeyError: if the symbol could not be found. """ return self._classes[self.pool.FindMessageTypeByName(symbol)] def GetMessages(self, files): # TODO(amauryfa): Fix the differences with MessageFactory. """Gets all registered messages from a specified file. Only messages already created and registered will be returned; (this is the case for imported _pb2 modules) But unlike MessageFactory, this version also returns already defined nested messages, but does not register any message extensions. Args: files (list[str]): The file names to extract messages from. Returns: A dictionary mapping proto names to the message classes. Raises: KeyError: if a file could not be found. """ def _GetAllMessages(desc): """Walk a message Descriptor and recursively yields all message names.""" yield desc for msg_desc in desc.nested_types: for nested_desc in _GetAllMessages(msg_desc): yield nested_desc result = {} for file_name in files: file_desc = self.pool.FindFileByName(file_name) for msg_desc in file_desc.message_types_by_name.values(): for desc in _GetAllMessages(msg_desc): try: result[desc.full_name] = self._classes[desc] except KeyError: # This descriptor has no registered class, skip it. pass return result _DEFAULT = SymbolDatabase(pool=descriptor_pool.Default()) def Default(): """Returns the default SymbolDatabase.""" return _DEFAULT ================================================ FILE: openpype/hosts/nuke/vendor/google/protobuf/text_encoding.py ================================================ # Protocol Buffers - Google's data interchange format # Copyright 2008 Google Inc. All rights reserved. # https://developers.google.com/protocol-buffers/ # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are # met: # # * Redistributions of source code must retain the above copyright # notice, this list of conditions and the following disclaimer. # * Redistributions in binary form must reproduce the above # copyright notice, this list of conditions and the following disclaimer # in the documentation and/or other materials provided with the # distribution. # * Neither the name of Google Inc. nor the names of its # contributors may be used to endorse or promote products derived from # this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. """Encoding related utilities.""" import re _cescape_chr_to_symbol_map = {} _cescape_chr_to_symbol_map[9] = r'\t' # optional escape _cescape_chr_to_symbol_map[10] = r'\n' # optional escape _cescape_chr_to_symbol_map[13] = r'\r' # optional escape _cescape_chr_to_symbol_map[34] = r'\"' # necessary escape _cescape_chr_to_symbol_map[39] = r"\'" # optional escape _cescape_chr_to_symbol_map[92] = r'\\' # necessary escape # Lookup table for unicode _cescape_unicode_to_str = [chr(i) for i in range(0, 256)] for byte, string in _cescape_chr_to_symbol_map.items(): _cescape_unicode_to_str[byte] = string # Lookup table for non-utf8, with necessary escapes at (o >= 127 or o < 32) _cescape_byte_to_str = ([r'\%03o' % i for i in range(0, 32)] + [chr(i) for i in range(32, 127)] + [r'\%03o' % i for i in range(127, 256)]) for byte, string in _cescape_chr_to_symbol_map.items(): _cescape_byte_to_str[byte] = string del byte, string def CEscape(text, as_utf8): # type: (...) -> str """Escape a bytes string for use in an text protocol buffer. Args: text: A byte string to be escaped. as_utf8: Specifies if result may contain non-ASCII characters. In Python 3 this allows unescaped non-ASCII Unicode characters. In Python 2 the return value will be valid UTF-8 rather than only ASCII. Returns: Escaped string (str). """ # Python's text.encode() 'string_escape' or 'unicode_escape' codecs do not # satisfy our needs; they encodes unprintable characters using two-digit hex # escapes whereas our C++ unescaping function allows hex escapes to be any # length. So, "\0011".encode('string_escape') ends up being "\\x011", which # will be decoded in C++ as a single-character string with char code 0x11. text_is_unicode = isinstance(text, str) if as_utf8 and text_is_unicode: # We're already unicode, no processing beyond control char escapes. return text.translate(_cescape_chr_to_symbol_map) ord_ = ord if text_is_unicode else lambda x: x # bytes iterate as ints. if as_utf8: return ''.join(_cescape_unicode_to_str[ord_(c)] for c in text) return ''.join(_cescape_byte_to_str[ord_(c)] for c in text) _CUNESCAPE_HEX = re.compile(r'(\\+)x([0-9a-fA-F])(?![0-9a-fA-F])') def CUnescape(text): # type: (str) -> bytes """Unescape a text string with C-style escape sequences to UTF-8 bytes. Args: text: The data to parse in a str. Returns: A byte string. """ def ReplaceHex(m): # Only replace the match if the number of leading back slashes is odd. i.e. # the slash itself is not escaped. if len(m.group(1)) & 1: return m.group(1) + 'x0' + m.group(2) return m.group(0) # This is required because the 'string_escape' encoding doesn't # allow single-digit hex escapes (like '\xf'). result = _CUNESCAPE_HEX.sub(ReplaceHex, text) return (result.encode('utf-8') # Make it bytes to allow decode. .decode('unicode_escape') # Make it bytes again to return the proper type. .encode('raw_unicode_escape')) ================================================ FILE: openpype/hosts/nuke/vendor/google/protobuf/text_format.py ================================================ # Protocol Buffers - Google's data interchange format # Copyright 2008 Google Inc. All rights reserved. # https://developers.google.com/protocol-buffers/ # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are # met: # # * Redistributions of source code must retain the above copyright # notice, this list of conditions and the following disclaimer. # * Redistributions in binary form must reproduce the above # copyright notice, this list of conditions and the following disclaimer # in the documentation and/or other materials provided with the # distribution. # * Neither the name of Google Inc. nor the names of its # contributors may be used to endorse or promote products derived from # this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. """Contains routines for printing protocol messages in text format. Simple usage example:: # Create a proto object and serialize it to a text proto string. message = my_proto_pb2.MyMessage(foo='bar') text_proto = text_format.MessageToString(message) # Parse a text proto string. message = text_format.Parse(text_proto, my_proto_pb2.MyMessage()) """ __author__ = 'kenton@google.com (Kenton Varda)' # TODO(b/129989314) Import thread contention leads to test failures. import encodings.raw_unicode_escape # pylint: disable=unused-import import encodings.unicode_escape # pylint: disable=unused-import import io import math import re from google.protobuf.internal import decoder from google.protobuf.internal import type_checkers from google.protobuf import descriptor from google.protobuf import text_encoding # pylint: disable=g-import-not-at-top __all__ = ['MessageToString', 'Parse', 'PrintMessage', 'PrintField', 'PrintFieldValue', 'Merge', 'MessageToBytes'] _INTEGER_CHECKERS = (type_checkers.Uint32ValueChecker(), type_checkers.Int32ValueChecker(), type_checkers.Uint64ValueChecker(), type_checkers.Int64ValueChecker()) _FLOAT_INFINITY = re.compile('-?inf(?:inity)?f?$', re.IGNORECASE) _FLOAT_NAN = re.compile('nanf?$', re.IGNORECASE) _QUOTES = frozenset(("'", '"')) _ANY_FULL_TYPE_NAME = 'google.protobuf.Any' class Error(Exception): """Top-level module error for text_format.""" class ParseError(Error): """Thrown in case of text parsing or tokenizing error.""" def __init__(self, message=None, line=None, column=None): if message is not None and line is not None: loc = str(line) if column is not None: loc += ':{0}'.format(column) message = '{0} : {1}'.format(loc, message) if message is not None: super(ParseError, self).__init__(message) else: super(ParseError, self).__init__() self._line = line self._column = column def GetLine(self): return self._line def GetColumn(self): return self._column class TextWriter(object): def __init__(self, as_utf8): self._writer = io.StringIO() def write(self, val): return self._writer.write(val) def close(self): return self._writer.close() def getvalue(self): return self._writer.getvalue() def MessageToString( message, as_utf8=False, as_one_line=False, use_short_repeated_primitives=False, pointy_brackets=False, use_index_order=False, float_format=None, double_format=None, use_field_number=False, descriptor_pool=None, indent=0, message_formatter=None, print_unknown_fields=False, force_colon=False): # type: (...) -> str """Convert protobuf message to text format. Double values can be formatted compactly with 15 digits of precision (which is the most that IEEE 754 "double" can guarantee) using double_format='.15g'. To ensure that converting to text and back to a proto will result in an identical value, double_format='.17g' should be used. Args: message: The protocol buffers message. as_utf8: Return unescaped Unicode for non-ASCII characters. In Python 3 actual Unicode characters may appear as is in strings. In Python 2 the return value will be valid UTF-8 rather than only ASCII. as_one_line: Don't introduce newlines between fields. use_short_repeated_primitives: Use short repeated format for primitives. pointy_brackets: If True, use angle brackets instead of curly braces for nesting. use_index_order: If True, fields of a proto message will be printed using the order defined in source code instead of the field number, extensions will be printed at the end of the message and their relative order is determined by the extension number. By default, use the field number order. float_format (str): If set, use this to specify float field formatting (per the "Format Specification Mini-Language"); otherwise, shortest float that has same value in wire will be printed. Also affect double field if double_format is not set but float_format is set. double_format (str): If set, use this to specify double field formatting (per the "Format Specification Mini-Language"); if it is not set but float_format is set, use float_format. Otherwise, use ``str()`` use_field_number: If True, print field numbers instead of names. descriptor_pool (DescriptorPool): Descriptor pool used to resolve Any types. indent (int): The initial indent level, in terms of spaces, for pretty print. message_formatter (function(message, indent, as_one_line) -> unicode|None): Custom formatter for selected sub-messages (usually based on message type). Use to pretty print parts of the protobuf for easier diffing. print_unknown_fields: If True, unknown fields will be printed. force_colon: If set, a colon will be added after the field name even if the field is a proto message. Returns: str: A string of the text formatted protocol buffer message. """ out = TextWriter(as_utf8) printer = _Printer( out, indent, as_utf8, as_one_line, use_short_repeated_primitives, pointy_brackets, use_index_order, float_format, double_format, use_field_number, descriptor_pool, message_formatter, print_unknown_fields=print_unknown_fields, force_colon=force_colon) printer.PrintMessage(message) result = out.getvalue() out.close() if as_one_line: return result.rstrip() return result def MessageToBytes(message, **kwargs): # type: (...) -> bytes """Convert protobuf message to encoded text format. See MessageToString.""" text = MessageToString(message, **kwargs) if isinstance(text, bytes): return text codec = 'utf-8' if kwargs.get('as_utf8') else 'ascii' return text.encode(codec) def _IsMapEntry(field): return (field.type == descriptor.FieldDescriptor.TYPE_MESSAGE and field.message_type.has_options and field.message_type.GetOptions().map_entry) def PrintMessage(message, out, indent=0, as_utf8=False, as_one_line=False, use_short_repeated_primitives=False, pointy_brackets=False, use_index_order=False, float_format=None, double_format=None, use_field_number=False, descriptor_pool=None, message_formatter=None, print_unknown_fields=False, force_colon=False): printer = _Printer( out=out, indent=indent, as_utf8=as_utf8, as_one_line=as_one_line, use_short_repeated_primitives=use_short_repeated_primitives, pointy_brackets=pointy_brackets, use_index_order=use_index_order, float_format=float_format, double_format=double_format, use_field_number=use_field_number, descriptor_pool=descriptor_pool, message_formatter=message_formatter, print_unknown_fields=print_unknown_fields, force_colon=force_colon) printer.PrintMessage(message) def PrintField(field, value, out, indent=0, as_utf8=False, as_one_line=False, use_short_repeated_primitives=False, pointy_brackets=False, use_index_order=False, float_format=None, double_format=None, message_formatter=None, print_unknown_fields=False, force_colon=False): """Print a single field name/value pair.""" printer = _Printer(out, indent, as_utf8, as_one_line, use_short_repeated_primitives, pointy_brackets, use_index_order, float_format, double_format, message_formatter=message_formatter, print_unknown_fields=print_unknown_fields, force_colon=force_colon) printer.PrintField(field, value) def PrintFieldValue(field, value, out, indent=0, as_utf8=False, as_one_line=False, use_short_repeated_primitives=False, pointy_brackets=False, use_index_order=False, float_format=None, double_format=None, message_formatter=None, print_unknown_fields=False, force_colon=False): """Print a single field value (not including name).""" printer = _Printer(out, indent, as_utf8, as_one_line, use_short_repeated_primitives, pointy_brackets, use_index_order, float_format, double_format, message_formatter=message_formatter, print_unknown_fields=print_unknown_fields, force_colon=force_colon) printer.PrintFieldValue(field, value) def _BuildMessageFromTypeName(type_name, descriptor_pool): """Returns a protobuf message instance. Args: type_name: Fully-qualified protobuf message type name string. descriptor_pool: DescriptorPool instance. Returns: A Message instance of type matching type_name, or None if the a Descriptor wasn't found matching type_name. """ # pylint: disable=g-import-not-at-top if descriptor_pool is None: from google.protobuf import descriptor_pool as pool_mod descriptor_pool = pool_mod.Default() from google.protobuf import symbol_database database = symbol_database.Default() try: message_descriptor = descriptor_pool.FindMessageTypeByName(type_name) except KeyError: return None message_type = database.GetPrototype(message_descriptor) return message_type() # These values must match WireType enum in google/protobuf/wire_format.h. WIRETYPE_LENGTH_DELIMITED = 2 WIRETYPE_START_GROUP = 3 class _Printer(object): """Text format printer for protocol message.""" def __init__( self, out, indent=0, as_utf8=False, as_one_line=False, use_short_repeated_primitives=False, pointy_brackets=False, use_index_order=False, float_format=None, double_format=None, use_field_number=False, descriptor_pool=None, message_formatter=None, print_unknown_fields=False, force_colon=False): """Initialize the Printer. Double values can be formatted compactly with 15 digits of precision (which is the most that IEEE 754 "double" can guarantee) using double_format='.15g'. To ensure that converting to text and back to a proto will result in an identical value, double_format='.17g' should be used. Args: out: To record the text format result. indent: The initial indent level for pretty print. as_utf8: Return unescaped Unicode for non-ASCII characters. In Python 3 actual Unicode characters may appear as is in strings. In Python 2 the return value will be valid UTF-8 rather than ASCII. as_one_line: Don't introduce newlines between fields. use_short_repeated_primitives: Use short repeated format for primitives. pointy_brackets: If True, use angle brackets instead of curly braces for nesting. use_index_order: If True, print fields of a proto message using the order defined in source code instead of the field number. By default, use the field number order. float_format: If set, use this to specify float field formatting (per the "Format Specification Mini-Language"); otherwise, shortest float that has same value in wire will be printed. Also affect double field if double_format is not set but float_format is set. double_format: If set, use this to specify double field formatting (per the "Format Specification Mini-Language"); if it is not set but float_format is set, use float_format. Otherwise, str() is used. use_field_number: If True, print field numbers instead of names. descriptor_pool: A DescriptorPool used to resolve Any types. message_formatter: A function(message, indent, as_one_line): unicode|None to custom format selected sub-messages (usually based on message type). Use to pretty print parts of the protobuf for easier diffing. print_unknown_fields: If True, unknown fields will be printed. force_colon: If set, a colon will be added after the field name even if the field is a proto message. """ self.out = out self.indent = indent self.as_utf8 = as_utf8 self.as_one_line = as_one_line self.use_short_repeated_primitives = use_short_repeated_primitives self.pointy_brackets = pointy_brackets self.use_index_order = use_index_order self.float_format = float_format if double_format is not None: self.double_format = double_format else: self.double_format = float_format self.use_field_number = use_field_number self.descriptor_pool = descriptor_pool self.message_formatter = message_formatter self.print_unknown_fields = print_unknown_fields self.force_colon = force_colon def _TryPrintAsAnyMessage(self, message): """Serializes if message is a google.protobuf.Any field.""" if '/' not in message.type_url: return False packed_message = _BuildMessageFromTypeName(message.TypeName(), self.descriptor_pool) if packed_message: packed_message.MergeFromString(message.value) colon = ':' if self.force_colon else '' self.out.write('%s[%s]%s ' % (self.indent * ' ', message.type_url, colon)) self._PrintMessageFieldValue(packed_message) self.out.write(' ' if self.as_one_line else '\n') return True else: return False def _TryCustomFormatMessage(self, message): formatted = self.message_formatter(message, self.indent, self.as_one_line) if formatted is None: return False out = self.out out.write(' ' * self.indent) out.write(formatted) out.write(' ' if self.as_one_line else '\n') return True def PrintMessage(self, message): """Convert protobuf message to text format. Args: message: The protocol buffers message. """ if self.message_formatter and self._TryCustomFormatMessage(message): return if (message.DESCRIPTOR.full_name == _ANY_FULL_TYPE_NAME and self._TryPrintAsAnyMessage(message)): return fields = message.ListFields() if self.use_index_order: fields.sort( key=lambda x: x[0].number if x[0].is_extension else x[0].index) for field, value in fields: if _IsMapEntry(field): for key in sorted(value): # This is slow for maps with submessage entries because it copies the # entire tree. Unfortunately this would take significant refactoring # of this file to work around. # # TODO(haberman): refactor and optimize if this becomes an issue. entry_submsg = value.GetEntryClass()(key=key, value=value[key]) self.PrintField(field, entry_submsg) elif field.label == descriptor.FieldDescriptor.LABEL_REPEATED: if (self.use_short_repeated_primitives and field.cpp_type != descriptor.FieldDescriptor.CPPTYPE_MESSAGE and field.cpp_type != descriptor.FieldDescriptor.CPPTYPE_STRING): self._PrintShortRepeatedPrimitivesValue(field, value) else: for element in value: self.PrintField(field, element) else: self.PrintField(field, value) if self.print_unknown_fields: self._PrintUnknownFields(message.UnknownFields()) def _PrintUnknownFields(self, unknown_fields): """Print unknown fields.""" out = self.out for field in unknown_fields: out.write(' ' * self.indent) out.write(str(field.field_number)) if field.wire_type == WIRETYPE_START_GROUP: if self.as_one_line: out.write(' { ') else: out.write(' {\n') self.indent += 2 self._PrintUnknownFields(field.data) if self.as_one_line: out.write('} ') else: self.indent -= 2 out.write(' ' * self.indent + '}\n') elif field.wire_type == WIRETYPE_LENGTH_DELIMITED: try: # If this field is parseable as a Message, it is probably # an embedded message. # pylint: disable=protected-access (embedded_unknown_message, pos) = decoder._DecodeUnknownFieldSet( memoryview(field.data), 0, len(field.data)) except Exception: # pylint: disable=broad-except pos = 0 if pos == len(field.data): if self.as_one_line: out.write(' { ') else: out.write(' {\n') self.indent += 2 self._PrintUnknownFields(embedded_unknown_message) if self.as_one_line: out.write('} ') else: self.indent -= 2 out.write(' ' * self.indent + '}\n') else: # A string or bytes field. self.as_utf8 may not work. out.write(': \"') out.write(text_encoding.CEscape(field.data, False)) out.write('\" ' if self.as_one_line else '\"\n') else: # varint, fixed32, fixed64 out.write(': ') out.write(str(field.data)) out.write(' ' if self.as_one_line else '\n') def _PrintFieldName(self, field): """Print field name.""" out = self.out out.write(' ' * self.indent) if self.use_field_number: out.write(str(field.number)) else: if field.is_extension: out.write('[') if (field.containing_type.GetOptions().message_set_wire_format and field.type == descriptor.FieldDescriptor.TYPE_MESSAGE and field.label == descriptor.FieldDescriptor.LABEL_OPTIONAL): out.write(field.message_type.full_name) else: out.write(field.full_name) out.write(']') elif field.type == descriptor.FieldDescriptor.TYPE_GROUP: # For groups, use the capitalized name. out.write(field.message_type.name) else: out.write(field.name) if (self.force_colon or field.cpp_type != descriptor.FieldDescriptor.CPPTYPE_MESSAGE): # The colon is optional in this case, but our cross-language golden files # don't include it. Here, the colon is only included if force_colon is # set to True out.write(':') def PrintField(self, field, value): """Print a single field name/value pair.""" self._PrintFieldName(field) self.out.write(' ') self.PrintFieldValue(field, value) self.out.write(' ' if self.as_one_line else '\n') def _PrintShortRepeatedPrimitivesValue(self, field, value): """"Prints short repeated primitives value.""" # Note: this is called only when value has at least one element. self._PrintFieldName(field) self.out.write(' [') for i in range(len(value) - 1): self.PrintFieldValue(field, value[i]) self.out.write(', ') self.PrintFieldValue(field, value[-1]) self.out.write(']') self.out.write(' ' if self.as_one_line else '\n') def _PrintMessageFieldValue(self, value): if self.pointy_brackets: openb = '<' closeb = '>' else: openb = '{' closeb = '}' if self.as_one_line: self.out.write('%s ' % openb) self.PrintMessage(value) self.out.write(closeb) else: self.out.write('%s\n' % openb) self.indent += 2 self.PrintMessage(value) self.indent -= 2 self.out.write(' ' * self.indent + closeb) def PrintFieldValue(self, field, value): """Print a single field value (not including name). For repeated fields, the value should be a single element. Args: field: The descriptor of the field to be printed. value: The value of the field. """ out = self.out if field.cpp_type == descriptor.FieldDescriptor.CPPTYPE_MESSAGE: self._PrintMessageFieldValue(value) elif field.cpp_type == descriptor.FieldDescriptor.CPPTYPE_ENUM: enum_value = field.enum_type.values_by_number.get(value, None) if enum_value is not None: out.write(enum_value.name) else: out.write(str(value)) elif field.cpp_type == descriptor.FieldDescriptor.CPPTYPE_STRING: out.write('\"') if isinstance(value, str) and not self.as_utf8: out_value = value.encode('utf-8') else: out_value = value if field.type == descriptor.FieldDescriptor.TYPE_BYTES: # We always need to escape all binary data in TYPE_BYTES fields. out_as_utf8 = False else: out_as_utf8 = self.as_utf8 out.write(text_encoding.CEscape(out_value, out_as_utf8)) out.write('\"') elif field.cpp_type == descriptor.FieldDescriptor.CPPTYPE_BOOL: if value: out.write('true') else: out.write('false') elif field.cpp_type == descriptor.FieldDescriptor.CPPTYPE_FLOAT: if self.float_format is not None: out.write('{1:{0}}'.format(self.float_format, value)) else: if math.isnan(value): out.write(str(value)) else: out.write(str(type_checkers.ToShortestFloat(value))) elif (field.cpp_type == descriptor.FieldDescriptor.CPPTYPE_DOUBLE and self.double_format is not None): out.write('{1:{0}}'.format(self.double_format, value)) else: out.write(str(value)) def Parse(text, message, allow_unknown_extension=False, allow_field_number=False, descriptor_pool=None, allow_unknown_field=False): """Parses a text representation of a protocol message into a message. NOTE: for historical reasons this function does not clear the input message. This is different from what the binary msg.ParseFrom(...) does. If text contains a field already set in message, the value is appended if the field is repeated. Otherwise, an error is raised. Example:: a = MyProto() a.repeated_field.append('test') b = MyProto() # Repeated fields are combined text_format.Parse(repr(a), b) text_format.Parse(repr(a), b) # repeated_field contains ["test", "test"] # Non-repeated fields cannot be overwritten a.singular_field = 1 b.singular_field = 2 text_format.Parse(repr(a), b) # ParseError # Binary version: b.ParseFromString(a.SerializeToString()) # repeated_field is now "test" Caller is responsible for clearing the message as needed. Args: text (str): Message text representation. message (Message): A protocol buffer message to merge into. allow_unknown_extension: if True, skip over missing extensions and keep parsing allow_field_number: if True, both field number and field name are allowed. descriptor_pool (DescriptorPool): Descriptor pool used to resolve Any types. allow_unknown_field: if True, skip over unknown field and keep parsing. Avoid to use this option if possible. It may hide some errors (e.g. spelling error on field name) Returns: Message: The same message passed as argument. Raises: ParseError: On text parsing problems. """ return ParseLines(text.split(b'\n' if isinstance(text, bytes) else u'\n'), message, allow_unknown_extension, allow_field_number, descriptor_pool=descriptor_pool, allow_unknown_field=allow_unknown_field) def Merge(text, message, allow_unknown_extension=False, allow_field_number=False, descriptor_pool=None, allow_unknown_field=False): """Parses a text representation of a protocol message into a message. Like Parse(), but allows repeated values for a non-repeated field, and uses the last one. This means any non-repeated, top-level fields specified in text replace those in the message. Args: text (str): Message text representation. message (Message): A protocol buffer message to merge into. allow_unknown_extension: if True, skip over missing extensions and keep parsing allow_field_number: if True, both field number and field name are allowed. descriptor_pool (DescriptorPool): Descriptor pool used to resolve Any types. allow_unknown_field: if True, skip over unknown field and keep parsing. Avoid to use this option if possible. It may hide some errors (e.g. spelling error on field name) Returns: Message: The same message passed as argument. Raises: ParseError: On text parsing problems. """ return MergeLines( text.split(b'\n' if isinstance(text, bytes) else u'\n'), message, allow_unknown_extension, allow_field_number, descriptor_pool=descriptor_pool, allow_unknown_field=allow_unknown_field) def ParseLines(lines, message, allow_unknown_extension=False, allow_field_number=False, descriptor_pool=None, allow_unknown_field=False): """Parses a text representation of a protocol message into a message. See Parse() for caveats. Args: lines: An iterable of lines of a message's text representation. message: A protocol buffer message to merge into. allow_unknown_extension: if True, skip over missing extensions and keep parsing allow_field_number: if True, both field number and field name are allowed. descriptor_pool: A DescriptorPool used to resolve Any types. allow_unknown_field: if True, skip over unknown field and keep parsing. Avoid to use this option if possible. It may hide some errors (e.g. spelling error on field name) Returns: The same message passed as argument. Raises: ParseError: On text parsing problems. """ parser = _Parser(allow_unknown_extension, allow_field_number, descriptor_pool=descriptor_pool, allow_unknown_field=allow_unknown_field) return parser.ParseLines(lines, message) def MergeLines(lines, message, allow_unknown_extension=False, allow_field_number=False, descriptor_pool=None, allow_unknown_field=False): """Parses a text representation of a protocol message into a message. See Merge() for more details. Args: lines: An iterable of lines of a message's text representation. message: A protocol buffer message to merge into. allow_unknown_extension: if True, skip over missing extensions and keep parsing allow_field_number: if True, both field number and field name are allowed. descriptor_pool: A DescriptorPool used to resolve Any types. allow_unknown_field: if True, skip over unknown field and keep parsing. Avoid to use this option if possible. It may hide some errors (e.g. spelling error on field name) Returns: The same message passed as argument. Raises: ParseError: On text parsing problems. """ parser = _Parser(allow_unknown_extension, allow_field_number, descriptor_pool=descriptor_pool, allow_unknown_field=allow_unknown_field) return parser.MergeLines(lines, message) class _Parser(object): """Text format parser for protocol message.""" def __init__(self, allow_unknown_extension=False, allow_field_number=False, descriptor_pool=None, allow_unknown_field=False): self.allow_unknown_extension = allow_unknown_extension self.allow_field_number = allow_field_number self.descriptor_pool = descriptor_pool self.allow_unknown_field = allow_unknown_field def ParseLines(self, lines, message): """Parses a text representation of a protocol message into a message.""" self._allow_multiple_scalars = False self._ParseOrMerge(lines, message) return message def MergeLines(self, lines, message): """Merges a text representation of a protocol message into a message.""" self._allow_multiple_scalars = True self._ParseOrMerge(lines, message) return message def _ParseOrMerge(self, lines, message): """Converts a text representation of a protocol message into a message. Args: lines: Lines of a message's text representation. message: A protocol buffer message to merge into. Raises: ParseError: On text parsing problems. """ # Tokenize expects native str lines. str_lines = ( line if isinstance(line, str) else line.decode('utf-8') for line in lines) tokenizer = Tokenizer(str_lines) while not tokenizer.AtEnd(): self._MergeField(tokenizer, message) def _MergeField(self, tokenizer, message): """Merges a single protocol message field into a message. Args: tokenizer: A tokenizer to parse the field name and values. message: A protocol message to record the data. Raises: ParseError: In case of text parsing problems. """ message_descriptor = message.DESCRIPTOR if (message_descriptor.full_name == _ANY_FULL_TYPE_NAME and tokenizer.TryConsume('[')): type_url_prefix, packed_type_name = self._ConsumeAnyTypeUrl(tokenizer) tokenizer.Consume(']') tokenizer.TryConsume(':') if tokenizer.TryConsume('<'): expanded_any_end_token = '>' else: tokenizer.Consume('{') expanded_any_end_token = '}' expanded_any_sub_message = _BuildMessageFromTypeName(packed_type_name, self.descriptor_pool) if not expanded_any_sub_message: raise ParseError('Type %s not found in descriptor pool' % packed_type_name) while not tokenizer.TryConsume(expanded_any_end_token): if tokenizer.AtEnd(): raise tokenizer.ParseErrorPreviousToken('Expected "%s".' % (expanded_any_end_token,)) self._MergeField(tokenizer, expanded_any_sub_message) deterministic = False message.Pack(expanded_any_sub_message, type_url_prefix=type_url_prefix, deterministic=deterministic) return if tokenizer.TryConsume('['): name = [tokenizer.ConsumeIdentifier()] while tokenizer.TryConsume('.'): name.append(tokenizer.ConsumeIdentifier()) name = '.'.join(name) if not message_descriptor.is_extendable: raise tokenizer.ParseErrorPreviousToken( 'Message type "%s" does not have extensions.' % message_descriptor.full_name) # pylint: disable=protected-access field = message.Extensions._FindExtensionByName(name) # pylint: enable=protected-access if not field: if self.allow_unknown_extension: field = None else: raise tokenizer.ParseErrorPreviousToken( 'Extension "%s" not registered. ' 'Did you import the _pb2 module which defines it? ' 'If you are trying to place the extension in the MessageSet ' 'field of another message that is in an Any or MessageSet field, ' 'that message\'s _pb2 module must be imported as well' % name) elif message_descriptor != field.containing_type: raise tokenizer.ParseErrorPreviousToken( 'Extension "%s" does not extend message type "%s".' % (name, message_descriptor.full_name)) tokenizer.Consume(']') else: name = tokenizer.ConsumeIdentifierOrNumber() if self.allow_field_number and name.isdigit(): number = ParseInteger(name, True, True) field = message_descriptor.fields_by_number.get(number, None) if not field and message_descriptor.is_extendable: field = message.Extensions._FindExtensionByNumber(number) else: field = message_descriptor.fields_by_name.get(name, None) # Group names are expected to be capitalized as they appear in the # .proto file, which actually matches their type names, not their field # names. if not field: field = message_descriptor.fields_by_name.get(name.lower(), None) if field and field.type != descriptor.FieldDescriptor.TYPE_GROUP: field = None if (field and field.type == descriptor.FieldDescriptor.TYPE_GROUP and field.message_type.name != name): field = None if not field and not self.allow_unknown_field: raise tokenizer.ParseErrorPreviousToken( 'Message type "%s" has no field named "%s".' % (message_descriptor.full_name, name)) if field: if not self._allow_multiple_scalars and field.containing_oneof: # Check if there's a different field set in this oneof. # Note that we ignore the case if the same field was set before, and we # apply _allow_multiple_scalars to non-scalar fields as well. which_oneof = message.WhichOneof(field.containing_oneof.name) if which_oneof is not None and which_oneof != field.name: raise tokenizer.ParseErrorPreviousToken( 'Field "%s" is specified along with field "%s", another member ' 'of oneof "%s" for message type "%s".' % (field.name, which_oneof, field.containing_oneof.name, message_descriptor.full_name)) if field.cpp_type == descriptor.FieldDescriptor.CPPTYPE_MESSAGE: tokenizer.TryConsume(':') merger = self._MergeMessageField else: tokenizer.Consume(':') merger = self._MergeScalarField if (field.label == descriptor.FieldDescriptor.LABEL_REPEATED and tokenizer.TryConsume('[')): # Short repeated format, e.g. "foo: [1, 2, 3]" if not tokenizer.TryConsume(']'): while True: merger(tokenizer, message, field) if tokenizer.TryConsume(']'): break tokenizer.Consume(',') else: merger(tokenizer, message, field) else: # Proto field is unknown. assert (self.allow_unknown_extension or self.allow_unknown_field) _SkipFieldContents(tokenizer) # For historical reasons, fields may optionally be separated by commas or # semicolons. if not tokenizer.TryConsume(','): tokenizer.TryConsume(';') def _ConsumeAnyTypeUrl(self, tokenizer): """Consumes a google.protobuf.Any type URL and returns the type name.""" # Consume "type.googleapis.com/". prefix = [tokenizer.ConsumeIdentifier()] tokenizer.Consume('.') prefix.append(tokenizer.ConsumeIdentifier()) tokenizer.Consume('.') prefix.append(tokenizer.ConsumeIdentifier()) tokenizer.Consume('/') # Consume the fully-qualified type name. name = [tokenizer.ConsumeIdentifier()] while tokenizer.TryConsume('.'): name.append(tokenizer.ConsumeIdentifier()) return '.'.join(prefix), '.'.join(name) def _MergeMessageField(self, tokenizer, message, field): """Merges a single scalar field into a message. Args: tokenizer: A tokenizer to parse the field value. message: The message of which field is a member. field: The descriptor of the field to be merged. Raises: ParseError: In case of text parsing problems. """ is_map_entry = _IsMapEntry(field) if tokenizer.TryConsume('<'): end_token = '>' else: tokenizer.Consume('{') end_token = '}' if field.label == descriptor.FieldDescriptor.LABEL_REPEATED: if field.is_extension: sub_message = message.Extensions[field].add() elif is_map_entry: sub_message = getattr(message, field.name).GetEntryClass()() else: sub_message = getattr(message, field.name).add() else: if field.is_extension: if (not self._allow_multiple_scalars and message.HasExtension(field)): raise tokenizer.ParseErrorPreviousToken( 'Message type "%s" should not have multiple "%s" extensions.' % (message.DESCRIPTOR.full_name, field.full_name)) sub_message = message.Extensions[field] else: # Also apply _allow_multiple_scalars to message field. # TODO(jieluo): Change to _allow_singular_overwrites. if (not self._allow_multiple_scalars and message.HasField(field.name)): raise tokenizer.ParseErrorPreviousToken( 'Message type "%s" should not have multiple "%s" fields.' % (message.DESCRIPTOR.full_name, field.name)) sub_message = getattr(message, field.name) sub_message.SetInParent() while not tokenizer.TryConsume(end_token): if tokenizer.AtEnd(): raise tokenizer.ParseErrorPreviousToken('Expected "%s".' % (end_token,)) self._MergeField(tokenizer, sub_message) if is_map_entry: value_cpptype = field.message_type.fields_by_name['value'].cpp_type if value_cpptype == descriptor.FieldDescriptor.CPPTYPE_MESSAGE: value = getattr(message, field.name)[sub_message.key] value.CopyFrom(sub_message.value) else: getattr(message, field.name)[sub_message.key] = sub_message.value @staticmethod def _IsProto3Syntax(message): message_descriptor = message.DESCRIPTOR return (hasattr(message_descriptor, 'syntax') and message_descriptor.syntax == 'proto3') def _MergeScalarField(self, tokenizer, message, field): """Merges a single scalar field into a message. Args: tokenizer: A tokenizer to parse the field value. message: A protocol message to record the data. field: The descriptor of the field to be merged. Raises: ParseError: In case of text parsing problems. RuntimeError: On runtime errors. """ _ = self.allow_unknown_extension value = None if field.type in (descriptor.FieldDescriptor.TYPE_INT32, descriptor.FieldDescriptor.TYPE_SINT32, descriptor.FieldDescriptor.TYPE_SFIXED32): value = _ConsumeInt32(tokenizer) elif field.type in (descriptor.FieldDescriptor.TYPE_INT64, descriptor.FieldDescriptor.TYPE_SINT64, descriptor.FieldDescriptor.TYPE_SFIXED64): value = _ConsumeInt64(tokenizer) elif field.type in (descriptor.FieldDescriptor.TYPE_UINT32, descriptor.FieldDescriptor.TYPE_FIXED32): value = _ConsumeUint32(tokenizer) elif field.type in (descriptor.FieldDescriptor.TYPE_UINT64, descriptor.FieldDescriptor.TYPE_FIXED64): value = _ConsumeUint64(tokenizer) elif field.type in (descriptor.FieldDescriptor.TYPE_FLOAT, descriptor.FieldDescriptor.TYPE_DOUBLE): value = tokenizer.ConsumeFloat() elif field.type == descriptor.FieldDescriptor.TYPE_BOOL: value = tokenizer.ConsumeBool() elif field.type == descriptor.FieldDescriptor.TYPE_STRING: value = tokenizer.ConsumeString() elif field.type == descriptor.FieldDescriptor.TYPE_BYTES: value = tokenizer.ConsumeByteString() elif field.type == descriptor.FieldDescriptor.TYPE_ENUM: value = tokenizer.ConsumeEnum(field) else: raise RuntimeError('Unknown field type %d' % field.type) if field.label == descriptor.FieldDescriptor.LABEL_REPEATED: if field.is_extension: message.Extensions[field].append(value) else: getattr(message, field.name).append(value) else: if field.is_extension: if (not self._allow_multiple_scalars and not self._IsProto3Syntax(message) and message.HasExtension(field)): raise tokenizer.ParseErrorPreviousToken( 'Message type "%s" should not have multiple "%s" extensions.' % (message.DESCRIPTOR.full_name, field.full_name)) else: message.Extensions[field] = value else: duplicate_error = False if not self._allow_multiple_scalars: if self._IsProto3Syntax(message): # Proto3 doesn't represent presence so we try best effort to check # multiple scalars by compare to default values. duplicate_error = bool(getattr(message, field.name)) else: duplicate_error = message.HasField(field.name) if duplicate_error: raise tokenizer.ParseErrorPreviousToken( 'Message type "%s" should not have multiple "%s" fields.' % (message.DESCRIPTOR.full_name, field.name)) else: setattr(message, field.name, value) def _SkipFieldContents(tokenizer): """Skips over contents (value or message) of a field. Args: tokenizer: A tokenizer to parse the field name and values. """ # Try to guess the type of this field. # If this field is not a message, there should be a ":" between the # field name and the field value and also the field value should not # start with "{" or "<" which indicates the beginning of a message body. # If there is no ":" or there is a "{" or "<" after ":", this field has # to be a message or the input is ill-formed. if tokenizer.TryConsume(':') and not tokenizer.LookingAt( '{') and not tokenizer.LookingAt('<'): _SkipFieldValue(tokenizer) else: _SkipFieldMessage(tokenizer) def _SkipField(tokenizer): """Skips over a complete field (name and value/message). Args: tokenizer: A tokenizer to parse the field name and values. """ if tokenizer.TryConsume('['): # Consume extension name. tokenizer.ConsumeIdentifier() while tokenizer.TryConsume('.'): tokenizer.ConsumeIdentifier() tokenizer.Consume(']') else: tokenizer.ConsumeIdentifierOrNumber() _SkipFieldContents(tokenizer) # For historical reasons, fields may optionally be separated by commas or # semicolons. if not tokenizer.TryConsume(','): tokenizer.TryConsume(';') def _SkipFieldMessage(tokenizer): """Skips over a field message. Args: tokenizer: A tokenizer to parse the field name and values. """ if tokenizer.TryConsume('<'): delimiter = '>' else: tokenizer.Consume('{') delimiter = '}' while not tokenizer.LookingAt('>') and not tokenizer.LookingAt('}'): _SkipField(tokenizer) tokenizer.Consume(delimiter) def _SkipFieldValue(tokenizer): """Skips over a field value. Args: tokenizer: A tokenizer to parse the field name and values. Raises: ParseError: In case an invalid field value is found. """ # String/bytes tokens can come in multiple adjacent string literals. # If we can consume one, consume as many as we can. if tokenizer.TryConsumeByteString(): while tokenizer.TryConsumeByteString(): pass return if (not tokenizer.TryConsumeIdentifier() and not _TryConsumeInt64(tokenizer) and not _TryConsumeUint64(tokenizer) and not tokenizer.TryConsumeFloat()): raise ParseError('Invalid field value: ' + tokenizer.token) class Tokenizer(object): """Protocol buffer text representation tokenizer. This class handles the lower level string parsing by splitting it into meaningful tokens. It was directly ported from the Java protocol buffer API. """ _WHITESPACE = re.compile(r'\s+') _COMMENT = re.compile(r'(\s*#.*$)', re.MULTILINE) _WHITESPACE_OR_COMMENT = re.compile(r'(\s|(#.*$))+', re.MULTILINE) _TOKEN = re.compile('|'.join([ r'[a-zA-Z_][0-9a-zA-Z_+-]*', # an identifier r'([0-9+-]|(\.[0-9]))[0-9a-zA-Z_.+-]*', # a number ] + [ # quoted str for each quote mark # Avoid backtracking! https://stackoverflow.com/a/844267 r'{qt}[^{qt}\n\\]*((\\.)+[^{qt}\n\\]*)*({qt}|\\?$)'.format(qt=mark) for mark in _QUOTES ])) _IDENTIFIER = re.compile(r'[^\d\W]\w*') _IDENTIFIER_OR_NUMBER = re.compile(r'\w+') def __init__(self, lines, skip_comments=True): self._position = 0 self._line = -1 self._column = 0 self._token_start = None self.token = '' self._lines = iter(lines) self._current_line = '' self._previous_line = 0 self._previous_column = 0 self._more_lines = True self._skip_comments = skip_comments self._whitespace_pattern = (skip_comments and self._WHITESPACE_OR_COMMENT or self._WHITESPACE) self._SkipWhitespace() self.NextToken() def LookingAt(self, token): return self.token == token def AtEnd(self): """Checks the end of the text was reached. Returns: True iff the end was reached. """ return not self.token def _PopLine(self): while len(self._current_line) <= self._column: try: self._current_line = next(self._lines) except StopIteration: self._current_line = '' self._more_lines = False return else: self._line += 1 self._column = 0 def _SkipWhitespace(self): while True: self._PopLine() match = self._whitespace_pattern.match(self._current_line, self._column) if not match: break length = len(match.group(0)) self._column += length def TryConsume(self, token): """Tries to consume a given piece of text. Args: token: Text to consume. Returns: True iff the text was consumed. """ if self.token == token: self.NextToken() return True return False def Consume(self, token): """Consumes a piece of text. Args: token: Text to consume. Raises: ParseError: If the text couldn't be consumed. """ if not self.TryConsume(token): raise self.ParseError('Expected "%s".' % token) def ConsumeComment(self): result = self.token if not self._COMMENT.match(result): raise self.ParseError('Expected comment.') self.NextToken() return result def ConsumeCommentOrTrailingComment(self): """Consumes a comment, returns a 2-tuple (trailing bool, comment str).""" # Tokenizer initializes _previous_line and _previous_column to 0. As the # tokenizer starts, it looks like there is a previous token on the line. just_started = self._line == 0 and self._column == 0 before_parsing = self._previous_line comment = self.ConsumeComment() # A trailing comment is a comment on the same line than the previous token. trailing = (self._previous_line == before_parsing and not just_started) return trailing, comment def TryConsumeIdentifier(self): try: self.ConsumeIdentifier() return True except ParseError: return False def ConsumeIdentifier(self): """Consumes protocol message field identifier. Returns: Identifier string. Raises: ParseError: If an identifier couldn't be consumed. """ result = self.token if not self._IDENTIFIER.match(result): raise self.ParseError('Expected identifier.') self.NextToken() return result def TryConsumeIdentifierOrNumber(self): try: self.ConsumeIdentifierOrNumber() return True except ParseError: return False def ConsumeIdentifierOrNumber(self): """Consumes protocol message field identifier. Returns: Identifier string. Raises: ParseError: If an identifier couldn't be consumed. """ result = self.token if not self._IDENTIFIER_OR_NUMBER.match(result): raise self.ParseError('Expected identifier or number, got %s.' % result) self.NextToken() return result def TryConsumeInteger(self): try: self.ConsumeInteger() return True except ParseError: return False def ConsumeInteger(self): """Consumes an integer number. Returns: The integer parsed. Raises: ParseError: If an integer couldn't be consumed. """ try: result = _ParseAbstractInteger(self.token) except ValueError as e: raise self.ParseError(str(e)) self.NextToken() return result def TryConsumeFloat(self): try: self.ConsumeFloat() return True except ParseError: return False def ConsumeFloat(self): """Consumes an floating point number. Returns: The number parsed. Raises: ParseError: If a floating point number couldn't be consumed. """ try: result = ParseFloat(self.token) except ValueError as e: raise self.ParseError(str(e)) self.NextToken() return result def ConsumeBool(self): """Consumes a boolean value. Returns: The bool parsed. Raises: ParseError: If a boolean value couldn't be consumed. """ try: result = ParseBool(self.token) except ValueError as e: raise self.ParseError(str(e)) self.NextToken() return result def TryConsumeByteString(self): try: self.ConsumeByteString() return True except ParseError: return False def ConsumeString(self): """Consumes a string value. Returns: The string parsed. Raises: ParseError: If a string value couldn't be consumed. """ the_bytes = self.ConsumeByteString() try: return str(the_bytes, 'utf-8') except UnicodeDecodeError as e: raise self._StringParseError(e) def ConsumeByteString(self): """Consumes a byte array value. Returns: The array parsed (as a string). Raises: ParseError: If a byte array value couldn't be consumed. """ the_list = [self._ConsumeSingleByteString()] while self.token and self.token[0] in _QUOTES: the_list.append(self._ConsumeSingleByteString()) return b''.join(the_list) def _ConsumeSingleByteString(self): """Consume one token of a string literal. String literals (whether bytes or text) can come in multiple adjacent tokens which are automatically concatenated, like in C or Python. This method only consumes one token. Returns: The token parsed. Raises: ParseError: When the wrong format data is found. """ text = self.token if len(text) < 1 or text[0] not in _QUOTES: raise self.ParseError('Expected string but found: %r' % (text,)) if len(text) < 2 or text[-1] != text[0]: raise self.ParseError('String missing ending quote: %r' % (text,)) try: result = text_encoding.CUnescape(text[1:-1]) except ValueError as e: raise self.ParseError(str(e)) self.NextToken() return result def ConsumeEnum(self, field): try: result = ParseEnum(field, self.token) except ValueError as e: raise self.ParseError(str(e)) self.NextToken() return result def ParseErrorPreviousToken(self, message): """Creates and *returns* a ParseError for the previously read token. Args: message: A message to set for the exception. Returns: A ParseError instance. """ return ParseError(message, self._previous_line + 1, self._previous_column + 1) def ParseError(self, message): """Creates and *returns* a ParseError for the current token.""" return ParseError('\'' + self._current_line + '\': ' + message, self._line + 1, self._column + 1) def _StringParseError(self, e): return self.ParseError('Couldn\'t parse string: ' + str(e)) def NextToken(self): """Reads the next meaningful token.""" self._previous_line = self._line self._previous_column = self._column self._column += len(self.token) self._SkipWhitespace() if not self._more_lines: self.token = '' return match = self._TOKEN.match(self._current_line, self._column) if not match and not self._skip_comments: match = self._COMMENT.match(self._current_line, self._column) if match: token = match.group(0) self.token = token else: self.token = self._current_line[self._column] # Aliased so it can still be accessed by current visibility violators. # TODO(dbarnett): Migrate violators to textformat_tokenizer. _Tokenizer = Tokenizer # pylint: disable=invalid-name def _ConsumeInt32(tokenizer): """Consumes a signed 32bit integer number from tokenizer. Args: tokenizer: A tokenizer used to parse the number. Returns: The integer parsed. Raises: ParseError: If a signed 32bit integer couldn't be consumed. """ return _ConsumeInteger(tokenizer, is_signed=True, is_long=False) def _ConsumeUint32(tokenizer): """Consumes an unsigned 32bit integer number from tokenizer. Args: tokenizer: A tokenizer used to parse the number. Returns: The integer parsed. Raises: ParseError: If an unsigned 32bit integer couldn't be consumed. """ return _ConsumeInteger(tokenizer, is_signed=False, is_long=False) def _TryConsumeInt64(tokenizer): try: _ConsumeInt64(tokenizer) return True except ParseError: return False def _ConsumeInt64(tokenizer): """Consumes a signed 32bit integer number from tokenizer. Args: tokenizer: A tokenizer used to parse the number. Returns: The integer parsed. Raises: ParseError: If a signed 32bit integer couldn't be consumed. """ return _ConsumeInteger(tokenizer, is_signed=True, is_long=True) def _TryConsumeUint64(tokenizer): try: _ConsumeUint64(tokenizer) return True except ParseError: return False def _ConsumeUint64(tokenizer): """Consumes an unsigned 64bit integer number from tokenizer. Args: tokenizer: A tokenizer used to parse the number. Returns: The integer parsed. Raises: ParseError: If an unsigned 64bit integer couldn't be consumed. """ return _ConsumeInteger(tokenizer, is_signed=False, is_long=True) def _ConsumeInteger(tokenizer, is_signed=False, is_long=False): """Consumes an integer number from tokenizer. Args: tokenizer: A tokenizer used to parse the number. is_signed: True if a signed integer must be parsed. is_long: True if a long integer must be parsed. Returns: The integer parsed. Raises: ParseError: If an integer with given characteristics couldn't be consumed. """ try: result = ParseInteger(tokenizer.token, is_signed=is_signed, is_long=is_long) except ValueError as e: raise tokenizer.ParseError(str(e)) tokenizer.NextToken() return result def ParseInteger(text, is_signed=False, is_long=False): """Parses an integer. Args: text: The text to parse. is_signed: True if a signed integer must be parsed. is_long: True if a long integer must be parsed. Returns: The integer value. Raises: ValueError: Thrown Iff the text is not a valid integer. """ # Do the actual parsing. Exception handling is propagated to caller. result = _ParseAbstractInteger(text) # Check if the integer is sane. Exceptions handled by callers. checker = _INTEGER_CHECKERS[2 * int(is_long) + int(is_signed)] checker.CheckValue(result) return result def _ParseAbstractInteger(text): """Parses an integer without checking size/signedness. Args: text: The text to parse. Returns: The integer value. Raises: ValueError: Thrown Iff the text is not a valid integer. """ # Do the actual parsing. Exception handling is propagated to caller. orig_text = text c_octal_match = re.match(r'(-?)0(\d+)$', text) if c_octal_match: # Python 3 no longer supports 0755 octal syntax without the 'o', so # we always use the '0o' prefix for multi-digit numbers starting with 0. text = c_octal_match.group(1) + '0o' + c_octal_match.group(2) try: return int(text, 0) except ValueError: raise ValueError('Couldn\'t parse integer: %s' % orig_text) def ParseFloat(text): """Parse a floating point number. Args: text: Text to parse. Returns: The number parsed. Raises: ValueError: If a floating point number couldn't be parsed. """ try: # Assume Python compatible syntax. return float(text) except ValueError: # Check alternative spellings. if _FLOAT_INFINITY.match(text): if text[0] == '-': return float('-inf') else: return float('inf') elif _FLOAT_NAN.match(text): return float('nan') else: # assume '1.0f' format try: return float(text.rstrip('f')) except ValueError: raise ValueError('Couldn\'t parse float: %s' % text) def ParseBool(text): """Parse a boolean value. Args: text: Text to parse. Returns: Boolean values parsed Raises: ValueError: If text is not a valid boolean. """ if text in ('true', 't', '1', 'True'): return True elif text in ('false', 'f', '0', 'False'): return False else: raise ValueError('Expected "true" or "false".') def ParseEnum(field, value): """Parse an enum value. The value can be specified by a number (the enum value), or by a string literal (the enum name). Args: field: Enum field descriptor. value: String value. Returns: Enum value number. Raises: ValueError: If the enum value could not be parsed. """ enum_descriptor = field.enum_type try: number = int(value, 0) except ValueError: # Identifier. enum_value = enum_descriptor.values_by_name.get(value, None) if enum_value is None: raise ValueError('Enum type "%s" has no value named %s.' % (enum_descriptor.full_name, value)) else: # Numeric value. if hasattr(field.file, 'syntax'): # Attribute is checked for compatibility. if field.file.syntax == 'proto3': # Proto3 accept numeric unknown enums. return number enum_value = enum_descriptor.values_by_number.get(number, None) if enum_value is None: raise ValueError('Enum type "%s" has no value with number %d.' % (enum_descriptor.full_name, number)) return enum_value.number ================================================ FILE: openpype/hosts/nuke/vendor/google/protobuf/timestamp_pb2.py ================================================ # -*- coding: utf-8 -*- # Generated by the protocol buffer compiler. DO NOT EDIT! # source: google/protobuf/timestamp.proto """Generated protocol buffer code.""" from google.protobuf.internal import builder as _builder from google.protobuf import descriptor as _descriptor from google.protobuf import descriptor_pool as _descriptor_pool from google.protobuf import symbol_database as _symbol_database # @@protoc_insertion_point(imports) _sym_db = _symbol_database.Default() DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x1fgoogle/protobuf/timestamp.proto\x12\x0fgoogle.protobuf\"+\n\tTimestamp\x12\x0f\n\x07seconds\x18\x01 \x01(\x03\x12\r\n\x05nanos\x18\x02 \x01(\x05\x42\x85\x01\n\x13\x63om.google.protobufB\x0eTimestampProtoP\x01Z2google.golang.org/protobuf/types/known/timestamppb\xf8\x01\x01\xa2\x02\x03GPB\xaa\x02\x1eGoogle.Protobuf.WellKnownTypesb\x06proto3') _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals()) _builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'google.protobuf.timestamp_pb2', globals()) if _descriptor._USE_C_DESCRIPTORS == False: DESCRIPTOR._options = None DESCRIPTOR._serialized_options = b'\n\023com.google.protobufB\016TimestampProtoP\001Z2google.golang.org/protobuf/types/known/timestamppb\370\001\001\242\002\003GPB\252\002\036Google.Protobuf.WellKnownTypes' _TIMESTAMP._serialized_start=52 _TIMESTAMP._serialized_end=95 # @@protoc_insertion_point(module_scope) ================================================ FILE: openpype/hosts/nuke/vendor/google/protobuf/type_pb2.py ================================================ # -*- coding: utf-8 -*- # Generated by the protocol buffer compiler. DO NOT EDIT! # source: google/protobuf/type.proto """Generated protocol buffer code.""" from google.protobuf.internal import builder as _builder from google.protobuf import descriptor as _descriptor from google.protobuf import descriptor_pool as _descriptor_pool from google.protobuf import symbol_database as _symbol_database # @@protoc_insertion_point(imports) _sym_db = _symbol_database.Default() from google.protobuf import any_pb2 as google_dot_protobuf_dot_any__pb2 from google.protobuf import source_context_pb2 as google_dot_protobuf_dot_source__context__pb2 DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x1agoogle/protobuf/type.proto\x12\x0fgoogle.protobuf\x1a\x19google/protobuf/any.proto\x1a$google/protobuf/source_context.proto\"\xd7\x01\n\x04Type\x12\x0c\n\x04name\x18\x01 \x01(\t\x12&\n\x06\x66ields\x18\x02 \x03(\x0b\x32\x16.google.protobuf.Field\x12\x0e\n\x06oneofs\x18\x03 \x03(\t\x12(\n\x07options\x18\x04 \x03(\x0b\x32\x17.google.protobuf.Option\x12\x36\n\x0esource_context\x18\x05 \x01(\x0b\x32\x1e.google.protobuf.SourceContext\x12\'\n\x06syntax\x18\x06 \x01(\x0e\x32\x17.google.protobuf.Syntax\"\xd5\x05\n\x05\x46ield\x12)\n\x04kind\x18\x01 \x01(\x0e\x32\x1b.google.protobuf.Field.Kind\x12\x37\n\x0b\x63\x61rdinality\x18\x02 \x01(\x0e\x32\".google.protobuf.Field.Cardinality\x12\x0e\n\x06number\x18\x03 \x01(\x05\x12\x0c\n\x04name\x18\x04 \x01(\t\x12\x10\n\x08type_url\x18\x06 \x01(\t\x12\x13\n\x0boneof_index\x18\x07 \x01(\x05\x12\x0e\n\x06packed\x18\x08 \x01(\x08\x12(\n\x07options\x18\t \x03(\x0b\x32\x17.google.protobuf.Option\x12\x11\n\tjson_name\x18\n \x01(\t\x12\x15\n\rdefault_value\x18\x0b \x01(\t\"\xc8\x02\n\x04Kind\x12\x10\n\x0cTYPE_UNKNOWN\x10\x00\x12\x0f\n\x0bTYPE_DOUBLE\x10\x01\x12\x0e\n\nTYPE_FLOAT\x10\x02\x12\x0e\n\nTYPE_INT64\x10\x03\x12\x0f\n\x0bTYPE_UINT64\x10\x04\x12\x0e\n\nTYPE_INT32\x10\x05\x12\x10\n\x0cTYPE_FIXED64\x10\x06\x12\x10\n\x0cTYPE_FIXED32\x10\x07\x12\r\n\tTYPE_BOOL\x10\x08\x12\x0f\n\x0bTYPE_STRING\x10\t\x12\x0e\n\nTYPE_GROUP\x10\n\x12\x10\n\x0cTYPE_MESSAGE\x10\x0b\x12\x0e\n\nTYPE_BYTES\x10\x0c\x12\x0f\n\x0bTYPE_UINT32\x10\r\x12\r\n\tTYPE_ENUM\x10\x0e\x12\x11\n\rTYPE_SFIXED32\x10\x0f\x12\x11\n\rTYPE_SFIXED64\x10\x10\x12\x0f\n\x0bTYPE_SINT32\x10\x11\x12\x0f\n\x0bTYPE_SINT64\x10\x12\"t\n\x0b\x43\x61rdinality\x12\x17\n\x13\x43\x41RDINALITY_UNKNOWN\x10\x00\x12\x18\n\x14\x43\x41RDINALITY_OPTIONAL\x10\x01\x12\x18\n\x14\x43\x41RDINALITY_REQUIRED\x10\x02\x12\x18\n\x14\x43\x41RDINALITY_REPEATED\x10\x03\"\xce\x01\n\x04\x45num\x12\x0c\n\x04name\x18\x01 \x01(\t\x12-\n\tenumvalue\x18\x02 \x03(\x0b\x32\x1a.google.protobuf.EnumValue\x12(\n\x07options\x18\x03 \x03(\x0b\x32\x17.google.protobuf.Option\x12\x36\n\x0esource_context\x18\x04 \x01(\x0b\x32\x1e.google.protobuf.SourceContext\x12\'\n\x06syntax\x18\x05 \x01(\x0e\x32\x17.google.protobuf.Syntax\"S\n\tEnumValue\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0e\n\x06number\x18\x02 \x01(\x05\x12(\n\x07options\x18\x03 \x03(\x0b\x32\x17.google.protobuf.Option\";\n\x06Option\x12\x0c\n\x04name\x18\x01 \x01(\t\x12#\n\x05value\x18\x02 \x01(\x0b\x32\x14.google.protobuf.Any*.\n\x06Syntax\x12\x11\n\rSYNTAX_PROTO2\x10\x00\x12\x11\n\rSYNTAX_PROTO3\x10\x01\x42{\n\x13\x63om.google.protobufB\tTypeProtoP\x01Z-google.golang.org/protobuf/types/known/typepb\xf8\x01\x01\xa2\x02\x03GPB\xaa\x02\x1eGoogle.Protobuf.WellKnownTypesb\x06proto3') _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals()) _builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'google.protobuf.type_pb2', globals()) if _descriptor._USE_C_DESCRIPTORS == False: DESCRIPTOR._options = None DESCRIPTOR._serialized_options = b'\n\023com.google.protobufB\tTypeProtoP\001Z-google.golang.org/protobuf/types/known/typepb\370\001\001\242\002\003GPB\252\002\036Google.Protobuf.WellKnownTypes' _SYNTAX._serialized_start=1413 _SYNTAX._serialized_end=1459 _TYPE._serialized_start=113 _TYPE._serialized_end=328 _FIELD._serialized_start=331 _FIELD._serialized_end=1056 _FIELD_KIND._serialized_start=610 _FIELD_KIND._serialized_end=938 _FIELD_CARDINALITY._serialized_start=940 _FIELD_CARDINALITY._serialized_end=1056 _ENUM._serialized_start=1059 _ENUM._serialized_end=1265 _ENUMVALUE._serialized_start=1267 _ENUMVALUE._serialized_end=1350 _OPTION._serialized_start=1352 _OPTION._serialized_end=1411 # @@protoc_insertion_point(module_scope) ================================================ FILE: openpype/hosts/nuke/vendor/google/protobuf/util/__init__.py ================================================ ================================================ FILE: openpype/hosts/nuke/vendor/google/protobuf/util/json_format_pb2.py ================================================ # -*- coding: utf-8 -*- # Generated by the protocol buffer compiler. DO NOT EDIT! # source: google/protobuf/util/json_format.proto """Generated protocol buffer code.""" from google.protobuf.internal import builder as _builder from google.protobuf import descriptor as _descriptor from google.protobuf import descriptor_pool as _descriptor_pool from google.protobuf import symbol_database as _symbol_database # @@protoc_insertion_point(imports) _sym_db = _symbol_database.Default() DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n&google/protobuf/util/json_format.proto\x12\x11protobuf_unittest\"\x89\x01\n\x13TestFlagsAndStrings\x12\t\n\x01\x41\x18\x01 \x02(\x05\x12K\n\rrepeatedgroup\x18\x02 \x03(\n24.protobuf_unittest.TestFlagsAndStrings.RepeatedGroup\x1a\x1a\n\rRepeatedGroup\x12\t\n\x01\x66\x18\x03 \x02(\t\"!\n\x14TestBase64ByteArrays\x12\t\n\x01\x61\x18\x01 \x02(\x0c\"G\n\x12TestJavaScriptJSON\x12\t\n\x01\x61\x18\x01 \x01(\x05\x12\r\n\x05\x66inal\x18\x02 \x01(\x02\x12\n\n\x02in\x18\x03 \x01(\t\x12\x0b\n\x03Var\x18\x04 \x01(\t\"Q\n\x18TestJavaScriptOrderJSON1\x12\t\n\x01\x64\x18\x01 \x01(\x05\x12\t\n\x01\x63\x18\x02 \x01(\x05\x12\t\n\x01x\x18\x03 \x01(\x08\x12\t\n\x01\x62\x18\x04 \x01(\x05\x12\t\n\x01\x61\x18\x05 \x01(\x05\"\x89\x01\n\x18TestJavaScriptOrderJSON2\x12\t\n\x01\x64\x18\x01 \x01(\x05\x12\t\n\x01\x63\x18\x02 \x01(\x05\x12\t\n\x01x\x18\x03 \x01(\x08\x12\t\n\x01\x62\x18\x04 \x01(\x05\x12\t\n\x01\x61\x18\x05 \x01(\x05\x12\x36\n\x01z\x18\x06 \x03(\x0b\x32+.protobuf_unittest.TestJavaScriptOrderJSON1\"$\n\x0cTestLargeInt\x12\t\n\x01\x61\x18\x01 \x02(\x03\x12\t\n\x01\x62\x18\x02 \x02(\x04\"\xa0\x01\n\x0bTestNumbers\x12\x30\n\x01\x61\x18\x01 \x01(\x0e\x32%.protobuf_unittest.TestNumbers.MyType\x12\t\n\x01\x62\x18\x02 \x01(\x05\x12\t\n\x01\x63\x18\x03 \x01(\x02\x12\t\n\x01\x64\x18\x04 \x01(\x08\x12\t\n\x01\x65\x18\x05 \x01(\x01\x12\t\n\x01\x66\x18\x06 \x01(\r\"(\n\x06MyType\x12\x06\n\x02OK\x10\x00\x12\x0b\n\x07WARNING\x10\x01\x12\t\n\x05\x45RROR\x10\x02\"T\n\rTestCamelCase\x12\x14\n\x0cnormal_field\x18\x01 \x01(\t\x12\x15\n\rCAPITAL_FIELD\x18\x02 \x01(\x05\x12\x16\n\x0e\x43\x61melCaseField\x18\x03 \x01(\x05\"|\n\x0bTestBoolMap\x12=\n\x08\x62ool_map\x18\x01 \x03(\x0b\x32+.protobuf_unittest.TestBoolMap.BoolMapEntry\x1a.\n\x0c\x42oolMapEntry\x12\x0b\n\x03key\x18\x01 \x01(\x08\x12\r\n\x05value\x18\x02 \x01(\x05:\x02\x38\x01\"O\n\rTestRecursion\x12\r\n\x05value\x18\x01 \x01(\x05\x12/\n\x05\x63hild\x18\x02 \x01(\x0b\x32 .protobuf_unittest.TestRecursion\"\x86\x01\n\rTestStringMap\x12\x43\n\nstring_map\x18\x01 \x03(\x0b\x32/.protobuf_unittest.TestStringMap.StringMapEntry\x1a\x30\n\x0eStringMapEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"\xc4\x01\n\x14TestStringSerializer\x12\x15\n\rscalar_string\x18\x01 \x01(\t\x12\x17\n\x0frepeated_string\x18\x02 \x03(\t\x12J\n\nstring_map\x18\x03 \x03(\x0b\x32\x36.protobuf_unittest.TestStringSerializer.StringMapEntry\x1a\x30\n\x0eStringMapEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"$\n\x18TestMessageWithExtension*\x08\x08\x64\x10\x80\x80\x80\x80\x02\"z\n\rTestExtension\x12\r\n\x05value\x18\x01 \x01(\t2Z\n\x03\x65xt\x12+.protobuf_unittest.TestMessageWithExtension\x18\x64 \x01(\x0b\x32 .protobuf_unittest.TestExtension\"Q\n\x14TestDefaultEnumValue\x12\x39\n\nenum_value\x18\x01 \x01(\x0e\x32\x1c.protobuf_unittest.EnumValue:\x07\x44\x45\x46\x41ULT*2\n\tEnumValue\x12\x0c\n\x08PROTOCOL\x10\x00\x12\n\n\x06\x42UFFER\x10\x01\x12\x0b\n\x07\x44\x45\x46\x41ULT\x10\x02') _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals()) _builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'google.protobuf.util.json_format_pb2', globals()) if _descriptor._USE_C_DESCRIPTORS == False: TestMessageWithExtension.RegisterExtension(_TESTEXTENSION.extensions_by_name['ext']) DESCRIPTOR._options = None _TESTBOOLMAP_BOOLMAPENTRY._options = None _TESTBOOLMAP_BOOLMAPENTRY._serialized_options = b'8\001' _TESTSTRINGMAP_STRINGMAPENTRY._options = None _TESTSTRINGMAP_STRINGMAPENTRY._serialized_options = b'8\001' _TESTSTRINGSERIALIZER_STRINGMAPENTRY._options = None _TESTSTRINGSERIALIZER_STRINGMAPENTRY._serialized_options = b'8\001' _ENUMVALUE._serialized_start=1607 _ENUMVALUE._serialized_end=1657 _TESTFLAGSANDSTRINGS._serialized_start=62 _TESTFLAGSANDSTRINGS._serialized_end=199 _TESTFLAGSANDSTRINGS_REPEATEDGROUP._serialized_start=173 _TESTFLAGSANDSTRINGS_REPEATEDGROUP._serialized_end=199 _TESTBASE64BYTEARRAYS._serialized_start=201 _TESTBASE64BYTEARRAYS._serialized_end=234 _TESTJAVASCRIPTJSON._serialized_start=236 _TESTJAVASCRIPTJSON._serialized_end=307 _TESTJAVASCRIPTORDERJSON1._serialized_start=309 _TESTJAVASCRIPTORDERJSON1._serialized_end=390 _TESTJAVASCRIPTORDERJSON2._serialized_start=393 _TESTJAVASCRIPTORDERJSON2._serialized_end=530 _TESTLARGEINT._serialized_start=532 _TESTLARGEINT._serialized_end=568 _TESTNUMBERS._serialized_start=571 _TESTNUMBERS._serialized_end=731 _TESTNUMBERS_MYTYPE._serialized_start=691 _TESTNUMBERS_MYTYPE._serialized_end=731 _TESTCAMELCASE._serialized_start=733 _TESTCAMELCASE._serialized_end=817 _TESTBOOLMAP._serialized_start=819 _TESTBOOLMAP._serialized_end=943 _TESTBOOLMAP_BOOLMAPENTRY._serialized_start=897 _TESTBOOLMAP_BOOLMAPENTRY._serialized_end=943 _TESTRECURSION._serialized_start=945 _TESTRECURSION._serialized_end=1024 _TESTSTRINGMAP._serialized_start=1027 _TESTSTRINGMAP._serialized_end=1161 _TESTSTRINGMAP_STRINGMAPENTRY._serialized_start=1113 _TESTSTRINGMAP_STRINGMAPENTRY._serialized_end=1161 _TESTSTRINGSERIALIZER._serialized_start=1164 _TESTSTRINGSERIALIZER._serialized_end=1360 _TESTSTRINGSERIALIZER_STRINGMAPENTRY._serialized_start=1113 _TESTSTRINGSERIALIZER_STRINGMAPENTRY._serialized_end=1161 _TESTMESSAGEWITHEXTENSION._serialized_start=1362 _TESTMESSAGEWITHEXTENSION._serialized_end=1398 _TESTEXTENSION._serialized_start=1400 _TESTEXTENSION._serialized_end=1522 _TESTDEFAULTENUMVALUE._serialized_start=1524 _TESTDEFAULTENUMVALUE._serialized_end=1605 # @@protoc_insertion_point(module_scope) ================================================ FILE: openpype/hosts/nuke/vendor/google/protobuf/util/json_format_proto3_pb2.py ================================================ # -*- coding: utf-8 -*- # Generated by the protocol buffer compiler. DO NOT EDIT! # source: google/protobuf/util/json_format_proto3.proto """Generated protocol buffer code.""" from google.protobuf.internal import builder as _builder from google.protobuf import descriptor as _descriptor from google.protobuf import descriptor_pool as _descriptor_pool from google.protobuf import symbol_database as _symbol_database # @@protoc_insertion_point(imports) _sym_db = _symbol_database.Default() from google.protobuf import any_pb2 as google_dot_protobuf_dot_any__pb2 from google.protobuf import duration_pb2 as google_dot_protobuf_dot_duration__pb2 from google.protobuf import field_mask_pb2 as google_dot_protobuf_dot_field__mask__pb2 from google.protobuf import struct_pb2 as google_dot_protobuf_dot_struct__pb2 from google.protobuf import timestamp_pb2 as google_dot_protobuf_dot_timestamp__pb2 from google.protobuf import wrappers_pb2 as google_dot_protobuf_dot_wrappers__pb2 from google.protobuf import unittest_pb2 as google_dot_protobuf_dot_unittest__pb2 DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n-google/protobuf/util/json_format_proto3.proto\x12\x06proto3\x1a\x19google/protobuf/any.proto\x1a\x1egoogle/protobuf/duration.proto\x1a google/protobuf/field_mask.proto\x1a\x1cgoogle/protobuf/struct.proto\x1a\x1fgoogle/protobuf/timestamp.proto\x1a\x1egoogle/protobuf/wrappers.proto\x1a\x1egoogle/protobuf/unittest.proto\"\x1c\n\x0bMessageType\x12\r\n\x05value\x18\x01 \x01(\x05\"\x94\x05\n\x0bTestMessage\x12\x12\n\nbool_value\x18\x01 \x01(\x08\x12\x13\n\x0bint32_value\x18\x02 \x01(\x05\x12\x13\n\x0bint64_value\x18\x03 \x01(\x03\x12\x14\n\x0cuint32_value\x18\x04 \x01(\r\x12\x14\n\x0cuint64_value\x18\x05 \x01(\x04\x12\x13\n\x0b\x66loat_value\x18\x06 \x01(\x02\x12\x14\n\x0c\x64ouble_value\x18\x07 \x01(\x01\x12\x14\n\x0cstring_value\x18\x08 \x01(\t\x12\x13\n\x0b\x62ytes_value\x18\t \x01(\x0c\x12$\n\nenum_value\x18\n \x01(\x0e\x32\x10.proto3.EnumType\x12*\n\rmessage_value\x18\x0b \x01(\x0b\x32\x13.proto3.MessageType\x12\x1b\n\x13repeated_bool_value\x18\x15 \x03(\x08\x12\x1c\n\x14repeated_int32_value\x18\x16 \x03(\x05\x12\x1c\n\x14repeated_int64_value\x18\x17 \x03(\x03\x12\x1d\n\x15repeated_uint32_value\x18\x18 \x03(\r\x12\x1d\n\x15repeated_uint64_value\x18\x19 \x03(\x04\x12\x1c\n\x14repeated_float_value\x18\x1a \x03(\x02\x12\x1d\n\x15repeated_double_value\x18\x1b \x03(\x01\x12\x1d\n\x15repeated_string_value\x18\x1c \x03(\t\x12\x1c\n\x14repeated_bytes_value\x18\x1d \x03(\x0c\x12-\n\x13repeated_enum_value\x18\x1e \x03(\x0e\x32\x10.proto3.EnumType\x12\x33\n\x16repeated_message_value\x18\x1f \x03(\x0b\x32\x13.proto3.MessageType\"\x8c\x02\n\tTestOneof\x12\x1b\n\x11oneof_int32_value\x18\x01 \x01(\x05H\x00\x12\x1c\n\x12oneof_string_value\x18\x02 \x01(\tH\x00\x12\x1b\n\x11oneof_bytes_value\x18\x03 \x01(\x0cH\x00\x12,\n\x10oneof_enum_value\x18\x04 \x01(\x0e\x32\x10.proto3.EnumTypeH\x00\x12\x32\n\x13oneof_message_value\x18\x05 \x01(\x0b\x32\x13.proto3.MessageTypeH\x00\x12\x36\n\x10oneof_null_value\x18\x06 \x01(\x0e\x32\x1a.google.protobuf.NullValueH\x00\x42\r\n\x0boneof_value\"\xe1\x04\n\x07TestMap\x12.\n\x08\x62ool_map\x18\x01 \x03(\x0b\x32\x1c.proto3.TestMap.BoolMapEntry\x12\x30\n\tint32_map\x18\x02 \x03(\x0b\x32\x1d.proto3.TestMap.Int32MapEntry\x12\x30\n\tint64_map\x18\x03 \x03(\x0b\x32\x1d.proto3.TestMap.Int64MapEntry\x12\x32\n\nuint32_map\x18\x04 \x03(\x0b\x32\x1e.proto3.TestMap.Uint32MapEntry\x12\x32\n\nuint64_map\x18\x05 \x03(\x0b\x32\x1e.proto3.TestMap.Uint64MapEntry\x12\x32\n\nstring_map\x18\x06 \x03(\x0b\x32\x1e.proto3.TestMap.StringMapEntry\x1a.\n\x0c\x42oolMapEntry\x12\x0b\n\x03key\x18\x01 \x01(\x08\x12\r\n\x05value\x18\x02 \x01(\x05:\x02\x38\x01\x1a/\n\rInt32MapEntry\x12\x0b\n\x03key\x18\x01 \x01(\x05\x12\r\n\x05value\x18\x02 \x01(\x05:\x02\x38\x01\x1a/\n\rInt64MapEntry\x12\x0b\n\x03key\x18\x01 \x01(\x03\x12\r\n\x05value\x18\x02 \x01(\x05:\x02\x38\x01\x1a\x30\n\x0eUint32MapEntry\x12\x0b\n\x03key\x18\x01 \x01(\r\x12\r\n\x05value\x18\x02 \x01(\x05:\x02\x38\x01\x1a\x30\n\x0eUint64MapEntry\x12\x0b\n\x03key\x18\x01 \x01(\x04\x12\r\n\x05value\x18\x02 \x01(\x05:\x02\x38\x01\x1a\x30\n\x0eStringMapEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\x05:\x02\x38\x01\"\x85\x06\n\rTestNestedMap\x12\x34\n\x08\x62ool_map\x18\x01 \x03(\x0b\x32\".proto3.TestNestedMap.BoolMapEntry\x12\x36\n\tint32_map\x18\x02 \x03(\x0b\x32#.proto3.TestNestedMap.Int32MapEntry\x12\x36\n\tint64_map\x18\x03 \x03(\x0b\x32#.proto3.TestNestedMap.Int64MapEntry\x12\x38\n\nuint32_map\x18\x04 \x03(\x0b\x32$.proto3.TestNestedMap.Uint32MapEntry\x12\x38\n\nuint64_map\x18\x05 \x03(\x0b\x32$.proto3.TestNestedMap.Uint64MapEntry\x12\x38\n\nstring_map\x18\x06 \x03(\x0b\x32$.proto3.TestNestedMap.StringMapEntry\x12\x32\n\x07map_map\x18\x07 \x03(\x0b\x32!.proto3.TestNestedMap.MapMapEntry\x1a.\n\x0c\x42oolMapEntry\x12\x0b\n\x03key\x18\x01 \x01(\x08\x12\r\n\x05value\x18\x02 \x01(\x05:\x02\x38\x01\x1a/\n\rInt32MapEntry\x12\x0b\n\x03key\x18\x01 \x01(\x05\x12\r\n\x05value\x18\x02 \x01(\x05:\x02\x38\x01\x1a/\n\rInt64MapEntry\x12\x0b\n\x03key\x18\x01 \x01(\x03\x12\r\n\x05value\x18\x02 \x01(\x05:\x02\x38\x01\x1a\x30\n\x0eUint32MapEntry\x12\x0b\n\x03key\x18\x01 \x01(\r\x12\r\n\x05value\x18\x02 \x01(\x05:\x02\x38\x01\x1a\x30\n\x0eUint64MapEntry\x12\x0b\n\x03key\x18\x01 \x01(\x04\x12\r\n\x05value\x18\x02 \x01(\x05:\x02\x38\x01\x1a\x30\n\x0eStringMapEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\x05:\x02\x38\x01\x1a\x44\n\x0bMapMapEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12$\n\x05value\x18\x02 \x01(\x0b\x32\x15.proto3.TestNestedMap:\x02\x38\x01\"{\n\rTestStringMap\x12\x38\n\nstring_map\x18\x01 \x03(\x0b\x32$.proto3.TestStringMap.StringMapEntry\x1a\x30\n\x0eStringMapEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"\xee\x07\n\x0bTestWrapper\x12.\n\nbool_value\x18\x01 \x01(\x0b\x32\x1a.google.protobuf.BoolValue\x12\x30\n\x0bint32_value\x18\x02 \x01(\x0b\x32\x1b.google.protobuf.Int32Value\x12\x30\n\x0bint64_value\x18\x03 \x01(\x0b\x32\x1b.google.protobuf.Int64Value\x12\x32\n\x0cuint32_value\x18\x04 \x01(\x0b\x32\x1c.google.protobuf.UInt32Value\x12\x32\n\x0cuint64_value\x18\x05 \x01(\x0b\x32\x1c.google.protobuf.UInt64Value\x12\x30\n\x0b\x66loat_value\x18\x06 \x01(\x0b\x32\x1b.google.protobuf.FloatValue\x12\x32\n\x0c\x64ouble_value\x18\x07 \x01(\x0b\x32\x1c.google.protobuf.DoubleValue\x12\x32\n\x0cstring_value\x18\x08 \x01(\x0b\x32\x1c.google.protobuf.StringValue\x12\x30\n\x0b\x62ytes_value\x18\t \x01(\x0b\x32\x1b.google.protobuf.BytesValue\x12\x37\n\x13repeated_bool_value\x18\x0b \x03(\x0b\x32\x1a.google.protobuf.BoolValue\x12\x39\n\x14repeated_int32_value\x18\x0c \x03(\x0b\x32\x1b.google.protobuf.Int32Value\x12\x39\n\x14repeated_int64_value\x18\r \x03(\x0b\x32\x1b.google.protobuf.Int64Value\x12;\n\x15repeated_uint32_value\x18\x0e \x03(\x0b\x32\x1c.google.protobuf.UInt32Value\x12;\n\x15repeated_uint64_value\x18\x0f \x03(\x0b\x32\x1c.google.protobuf.UInt64Value\x12\x39\n\x14repeated_float_value\x18\x10 \x03(\x0b\x32\x1b.google.protobuf.FloatValue\x12;\n\x15repeated_double_value\x18\x11 \x03(\x0b\x32\x1c.google.protobuf.DoubleValue\x12;\n\x15repeated_string_value\x18\x12 \x03(\x0b\x32\x1c.google.protobuf.StringValue\x12\x39\n\x14repeated_bytes_value\x18\x13 \x03(\x0b\x32\x1b.google.protobuf.BytesValue\"n\n\rTestTimestamp\x12)\n\x05value\x18\x01 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x32\n\x0erepeated_value\x18\x02 \x03(\x0b\x32\x1a.google.protobuf.Timestamp\"k\n\x0cTestDuration\x12(\n\x05value\x18\x01 \x01(\x0b\x32\x19.google.protobuf.Duration\x12\x31\n\x0erepeated_value\x18\x02 \x03(\x0b\x32\x19.google.protobuf.Duration\":\n\rTestFieldMask\x12)\n\x05value\x18\x01 \x01(\x0b\x32\x1a.google.protobuf.FieldMask\"e\n\nTestStruct\x12&\n\x05value\x18\x01 \x01(\x0b\x32\x17.google.protobuf.Struct\x12/\n\x0erepeated_value\x18\x02 \x03(\x0b\x32\x17.google.protobuf.Struct\"\\\n\x07TestAny\x12#\n\x05value\x18\x01 \x01(\x0b\x32\x14.google.protobuf.Any\x12,\n\x0erepeated_value\x18\x02 \x03(\x0b\x32\x14.google.protobuf.Any\"b\n\tTestValue\x12%\n\x05value\x18\x01 \x01(\x0b\x32\x16.google.protobuf.Value\x12.\n\x0erepeated_value\x18\x02 \x03(\x0b\x32\x16.google.protobuf.Value\"n\n\rTestListValue\x12)\n\x05value\x18\x01 \x01(\x0b\x32\x1a.google.protobuf.ListValue\x12\x32\n\x0erepeated_value\x18\x02 \x03(\x0b\x32\x1a.google.protobuf.ListValue\"\x89\x01\n\rTestBoolValue\x12\x12\n\nbool_value\x18\x01 \x01(\x08\x12\x34\n\x08\x62ool_map\x18\x02 \x03(\x0b\x32\".proto3.TestBoolValue.BoolMapEntry\x1a.\n\x0c\x42oolMapEntry\x12\x0b\n\x03key\x18\x01 \x01(\x08\x12\r\n\x05value\x18\x02 \x01(\x05:\x02\x38\x01\"+\n\x12TestCustomJsonName\x12\x15\n\x05value\x18\x01 \x01(\x05R\x06@value\"J\n\x0eTestExtensions\x12\x38\n\nextensions\x18\x01 \x01(\x0b\x32$.protobuf_unittest.TestAllExtensions\"\x84\x01\n\rTestEnumValue\x12%\n\x0b\x65num_value1\x18\x01 \x01(\x0e\x32\x10.proto3.EnumType\x12%\n\x0b\x65num_value2\x18\x02 \x01(\x0e\x32\x10.proto3.EnumType\x12%\n\x0b\x65num_value3\x18\x03 \x01(\x0e\x32\x10.proto3.EnumType*\x1c\n\x08\x45numType\x12\x07\n\x03\x46OO\x10\x00\x12\x07\n\x03\x42\x41R\x10\x01\x42,\n\x18\x63om.google.protobuf.utilB\x10JsonFormatProto3b\x06proto3') _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals()) _builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'google.protobuf.util.json_format_proto3_pb2', globals()) if _descriptor._USE_C_DESCRIPTORS == False: DESCRIPTOR._options = None DESCRIPTOR._serialized_options = b'\n\030com.google.protobuf.utilB\020JsonFormatProto3' _TESTMAP_BOOLMAPENTRY._options = None _TESTMAP_BOOLMAPENTRY._serialized_options = b'8\001' _TESTMAP_INT32MAPENTRY._options = None _TESTMAP_INT32MAPENTRY._serialized_options = b'8\001' _TESTMAP_INT64MAPENTRY._options = None _TESTMAP_INT64MAPENTRY._serialized_options = b'8\001' _TESTMAP_UINT32MAPENTRY._options = None _TESTMAP_UINT32MAPENTRY._serialized_options = b'8\001' _TESTMAP_UINT64MAPENTRY._options = None _TESTMAP_UINT64MAPENTRY._serialized_options = b'8\001' _TESTMAP_STRINGMAPENTRY._options = None _TESTMAP_STRINGMAPENTRY._serialized_options = b'8\001' _TESTNESTEDMAP_BOOLMAPENTRY._options = None _TESTNESTEDMAP_BOOLMAPENTRY._serialized_options = b'8\001' _TESTNESTEDMAP_INT32MAPENTRY._options = None _TESTNESTEDMAP_INT32MAPENTRY._serialized_options = b'8\001' _TESTNESTEDMAP_INT64MAPENTRY._options = None _TESTNESTEDMAP_INT64MAPENTRY._serialized_options = b'8\001' _TESTNESTEDMAP_UINT32MAPENTRY._options = None _TESTNESTEDMAP_UINT32MAPENTRY._serialized_options = b'8\001' _TESTNESTEDMAP_UINT64MAPENTRY._options = None _TESTNESTEDMAP_UINT64MAPENTRY._serialized_options = b'8\001' _TESTNESTEDMAP_STRINGMAPENTRY._options = None _TESTNESTEDMAP_STRINGMAPENTRY._serialized_options = b'8\001' _TESTNESTEDMAP_MAPMAPENTRY._options = None _TESTNESTEDMAP_MAPMAPENTRY._serialized_options = b'8\001' _TESTSTRINGMAP_STRINGMAPENTRY._options = None _TESTSTRINGMAP_STRINGMAPENTRY._serialized_options = b'8\001' _TESTBOOLVALUE_BOOLMAPENTRY._options = None _TESTBOOLVALUE_BOOLMAPENTRY._serialized_options = b'8\001' _ENUMTYPE._serialized_start=4849 _ENUMTYPE._serialized_end=4877 _MESSAGETYPE._serialized_start=277 _MESSAGETYPE._serialized_end=305 _TESTMESSAGE._serialized_start=308 _TESTMESSAGE._serialized_end=968 _TESTONEOF._serialized_start=971 _TESTONEOF._serialized_end=1239 _TESTMAP._serialized_start=1242 _TESTMAP._serialized_end=1851 _TESTMAP_BOOLMAPENTRY._serialized_start=1557 _TESTMAP_BOOLMAPENTRY._serialized_end=1603 _TESTMAP_INT32MAPENTRY._serialized_start=1605 _TESTMAP_INT32MAPENTRY._serialized_end=1652 _TESTMAP_INT64MAPENTRY._serialized_start=1654 _TESTMAP_INT64MAPENTRY._serialized_end=1701 _TESTMAP_UINT32MAPENTRY._serialized_start=1703 _TESTMAP_UINT32MAPENTRY._serialized_end=1751 _TESTMAP_UINT64MAPENTRY._serialized_start=1753 _TESTMAP_UINT64MAPENTRY._serialized_end=1801 _TESTMAP_STRINGMAPENTRY._serialized_start=1803 _TESTMAP_STRINGMAPENTRY._serialized_end=1851 _TESTNESTEDMAP._serialized_start=1854 _TESTNESTEDMAP._serialized_end=2627 _TESTNESTEDMAP_BOOLMAPENTRY._serialized_start=1557 _TESTNESTEDMAP_BOOLMAPENTRY._serialized_end=1603 _TESTNESTEDMAP_INT32MAPENTRY._serialized_start=1605 _TESTNESTEDMAP_INT32MAPENTRY._serialized_end=1652 _TESTNESTEDMAP_INT64MAPENTRY._serialized_start=1654 _TESTNESTEDMAP_INT64MAPENTRY._serialized_end=1701 _TESTNESTEDMAP_UINT32MAPENTRY._serialized_start=1703 _TESTNESTEDMAP_UINT32MAPENTRY._serialized_end=1751 _TESTNESTEDMAP_UINT64MAPENTRY._serialized_start=1753 _TESTNESTEDMAP_UINT64MAPENTRY._serialized_end=1801 _TESTNESTEDMAP_STRINGMAPENTRY._serialized_start=1803 _TESTNESTEDMAP_STRINGMAPENTRY._serialized_end=1851 _TESTNESTEDMAP_MAPMAPENTRY._serialized_start=2559 _TESTNESTEDMAP_MAPMAPENTRY._serialized_end=2627 _TESTSTRINGMAP._serialized_start=2629 _TESTSTRINGMAP._serialized_end=2752 _TESTSTRINGMAP_STRINGMAPENTRY._serialized_start=2704 _TESTSTRINGMAP_STRINGMAPENTRY._serialized_end=2752 _TESTWRAPPER._serialized_start=2755 _TESTWRAPPER._serialized_end=3761 _TESTTIMESTAMP._serialized_start=3763 _TESTTIMESTAMP._serialized_end=3873 _TESTDURATION._serialized_start=3875 _TESTDURATION._serialized_end=3982 _TESTFIELDMASK._serialized_start=3984 _TESTFIELDMASK._serialized_end=4042 _TESTSTRUCT._serialized_start=4044 _TESTSTRUCT._serialized_end=4145 _TESTANY._serialized_start=4147 _TESTANY._serialized_end=4239 _TESTVALUE._serialized_start=4241 _TESTVALUE._serialized_end=4339 _TESTLISTVALUE._serialized_start=4341 _TESTLISTVALUE._serialized_end=4451 _TESTBOOLVALUE._serialized_start=4454 _TESTBOOLVALUE._serialized_end=4591 _TESTBOOLVALUE_BOOLMAPENTRY._serialized_start=1557 _TESTBOOLVALUE_BOOLMAPENTRY._serialized_end=1603 _TESTCUSTOMJSONNAME._serialized_start=4593 _TESTCUSTOMJSONNAME._serialized_end=4636 _TESTEXTENSIONS._serialized_start=4638 _TESTEXTENSIONS._serialized_end=4712 _TESTENUMVALUE._serialized_start=4715 _TESTENUMVALUE._serialized_end=4847 # @@protoc_insertion_point(module_scope) ================================================ FILE: openpype/hosts/nuke/vendor/google/protobuf/wrappers_pb2.py ================================================ # -*- coding: utf-8 -*- # Generated by the protocol buffer compiler. DO NOT EDIT! # source: google/protobuf/wrappers.proto """Generated protocol buffer code.""" from google.protobuf.internal import builder as _builder from google.protobuf import descriptor as _descriptor from google.protobuf import descriptor_pool as _descriptor_pool from google.protobuf import symbol_database as _symbol_database # @@protoc_insertion_point(imports) _sym_db = _symbol_database.Default() DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x1egoogle/protobuf/wrappers.proto\x12\x0fgoogle.protobuf\"\x1c\n\x0b\x44oubleValue\x12\r\n\x05value\x18\x01 \x01(\x01\"\x1b\n\nFloatValue\x12\r\n\x05value\x18\x01 \x01(\x02\"\x1b\n\nInt64Value\x12\r\n\x05value\x18\x01 \x01(\x03\"\x1c\n\x0bUInt64Value\x12\r\n\x05value\x18\x01 \x01(\x04\"\x1b\n\nInt32Value\x12\r\n\x05value\x18\x01 \x01(\x05\"\x1c\n\x0bUInt32Value\x12\r\n\x05value\x18\x01 \x01(\r\"\x1a\n\tBoolValue\x12\r\n\x05value\x18\x01 \x01(\x08\"\x1c\n\x0bStringValue\x12\r\n\x05value\x18\x01 \x01(\t\"\x1b\n\nBytesValue\x12\r\n\x05value\x18\x01 \x01(\x0c\x42\x83\x01\n\x13\x63om.google.protobufB\rWrappersProtoP\x01Z1google.golang.org/protobuf/types/known/wrapperspb\xf8\x01\x01\xa2\x02\x03GPB\xaa\x02\x1eGoogle.Protobuf.WellKnownTypesb\x06proto3') _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals()) _builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'google.protobuf.wrappers_pb2', globals()) if _descriptor._USE_C_DESCRIPTORS == False: DESCRIPTOR._options = None DESCRIPTOR._serialized_options = b'\n\023com.google.protobufB\rWrappersProtoP\001Z1google.golang.org/protobuf/types/known/wrapperspb\370\001\001\242\002\003GPB\252\002\036Google.Protobuf.WellKnownTypes' _DOUBLEVALUE._serialized_start=51 _DOUBLEVALUE._serialized_end=79 _FLOATVALUE._serialized_start=81 _FLOATVALUE._serialized_end=108 _INT64VALUE._serialized_start=110 _INT64VALUE._serialized_end=137 _UINT64VALUE._serialized_start=139 _UINT64VALUE._serialized_end=167 _INT32VALUE._serialized_start=169 _INT32VALUE._serialized_end=196 _UINT32VALUE._serialized_start=198 _UINT32VALUE._serialized_end=226 _BOOLVALUE._serialized_start=228 _BOOLVALUE._serialized_end=254 _STRINGVALUE._serialized_start=256 _STRINGVALUE._serialized_end=284 _BYTESVALUE._serialized_start=286 _BYTESVALUE._serialized_end=313 # @@protoc_insertion_point(module_scope) ================================================ FILE: openpype/hosts/photoshop/__init__.py ================================================ from .addon import ( PhotoshopAddon, PHOTOSHOP_HOST_DIR, ) __all__ = ( "PhotoshopAddon", "PHOTOSHOP_HOST_DIR", ) ================================================ FILE: openpype/hosts/photoshop/addon.py ================================================ import os from openpype.modules import OpenPypeModule, IHostAddon PHOTOSHOP_HOST_DIR = os.path.dirname(os.path.abspath(__file__)) class PhotoshopAddon(OpenPypeModule, IHostAddon): name = "photoshop" host_name = "photoshop" def initialize(self, module_settings): self.enabled = True def add_implementation_envs(self, env, _app): """Modify environments to contain all required for implementation.""" defaults = { "OPENPYPE_LOG_NO_COLORS": "True", "WEBSOCKET_URL": "ws://localhost:8099/ws/" } for key, value in defaults.items(): if not env.get(key): env[key] = value def get_workfile_extensions(self): return [".psd", ".psb"] ================================================ FILE: openpype/hosts/photoshop/api/README.md ================================================ # Photoshop Integration ## Setup The Photoshop integration requires two components to work; `extension` and `server`. ### Extension To install the extension download [Extension Manager Command Line tool (ExManCmd)](https://github.com/Adobe-CEP/Getting-Started-guides/tree/master/Package%20Distribute%20Install#option-2---exmancmd). ``` ExManCmd /install {path to addon}/api/extension.zxp ``` ### Server The easiest way to get the server and Photoshop launch is with: ``` python -c ^"import openpype.hosts.photoshop;openpype.hosts.photoshop.launch(""C:\Program Files\Adobe\Adobe Photoshop 2020\Photoshop.exe"")^" ``` `avalon.photoshop.launch` launches the application and server, and also closes the server when Photoshop exists. ## Usage The Photoshop extension can be found under `Window > Extensions > Ayon`. Once launched you should be presented with a panel like this:  ## Developing ### Extension When developing the extension you can load it [unsigned](https://github.com/Adobe-CEP/CEP-Resources/blob/master/CEP_9.x/Documentation/CEP%209.0%20HTML%20Extension%20Cookbook.md#debugging-unsigned-extensions). When signing the extension you can use this [guide](https://github.com/Adobe-CEP/Getting-Started-guides/tree/master/Package%20Distribute%20Install#package-distribute-install-guide). ``` ZXPSignCmd -selfSignedCert NA NA Ayon Ayon-Photoshop Ayon extension.p12 ZXPSignCmd -sign {path to avalon-core}\avalon\photoshop\extension {path to avalon-core}\avalon\photoshop\extension.zxp extension.p12 avalon ``` ### Plugin Examples These plugins were made with the [polly config](https://github.com/mindbender-studio/config). To fully integrate and load, you will have to use this config and add `image` to the [integration plugin](https://github.com/mindbender-studio/config/blob/master/polly/plugins/publish/integrate_asset.py). #### Creator Plugin ```python from avalon import photoshop class CreateImage(photoshop.Creator): """Image folder for publish.""" name = "imageDefault" label = "Image" family = "image" def __init__(self, *args, **kwargs): super(CreateImage, self).__init__(*args, **kwargs) ``` #### Collector Plugin ```python import pythoncom import pyblish.api class CollectInstances(pyblish.api.ContextPlugin): """Gather instances by LayerSet and file metadata This collector takes into account assets that are associated with an LayerSet and marked with a unique identifier; Identifier: id (str): "pyblish.avalon.instance" """ label = "Instances" order = pyblish.api.CollectorOrder hosts = ["photoshop"] families_mapping = { "image": [] } def process(self, context): # Necessary call when running in a different thread which pyblish-qml # can be. pythoncom.CoInitialize() photoshop_client = PhotoshopClientStub() layers = photoshop_client.get_layers() layers_meta = photoshop_client.get_layers_metadata() for layer in layers: layer_data = photoshop_client.read(layer, layers_meta) # Skip layers without metadata. if layer_data is None: continue # Skip containers. if "container" in layer_data["id"]: continue # child_layers = [*layer.Layers] # self.log.debug("child_layers {}".format(child_layers)) # if not child_layers: # self.log.info("%s skipped, it was empty." % layer.Name) # continue instance = context.create_instance(layer.name) instance.append(layer) instance.data.update(layer_data) instance.data["families"] = self.families_mapping[ layer_data["family"] ] instance.data["publish"] = layer.visible # Produce diagnostic message for any graphical # user interface interested in visualising it. self.log.info("Found: \"%s\" " % instance.data["name"]) ``` #### Extractor Plugin ```python import os from openpype.pipeline import publish from openpype.hosts.photoshop import api as photoshop class ExtractImage(publish.Extractor): """Produce a flattened image file from instance This plug-in takes into account only the layers in the group. """ label = "Extract Image" hosts = ["photoshop"] families = ["image"] formats = ["png", "jpg"] def process(self, instance): staging_dir = self.staging_dir(instance) self.log.info("Outputting image to {}".format(staging_dir)) # Perform extraction stub = photoshop.stub() files = {} with photoshop.maintained_selection(): self.log.info("Extracting %s" % str(list(instance))) with photoshop.maintained_visibility(): # Hide all other layers. extract_ids = set([ll.id for ll in stub. get_layers_in_layers([instance[0]])]) for layer in stub.get_layers(): # limit unnecessary calls to client if layer.visible and layer.id not in extract_ids: stub.set_visible(layer.id, False) save_options = [] if "png" in self.formats: save_options.append('png') if "jpg" in self.formats: save_options.append('jpg') file_basename = os.path.splitext( stub.get_active_document_name() )[0] for extension in save_options: _filename = "{}.{}".format(file_basename, extension) files[extension] = _filename full_filename = os.path.join(staging_dir, _filename) stub.saveAs(full_filename, extension, True) representations = [] for extension, filename in files.items(): representations.append({ "name": extension, "ext": extension, "files": filename, "stagingDir": staging_dir }) instance.data["representations"] = representations instance.data["stagingDir"] = staging_dir self.log.info(f"Extracted {instance} to {staging_dir}") ``` #### Loader Plugin ```python from avalon import api, photoshop from openpype.pipeline import load, get_representation_path stub = photoshop.stub() class ImageLoader(load.LoaderPlugin): """Load images Stores the imported asset in a container named after the asset. """ families = ["image"] representations = ["*"] def load(self, context, name=None, namespace=None, data=None): path = self.filepath_from_context(context) with photoshop.maintained_selection(): layer = stub.import_smart_object(path) self[:] = [layer] return photoshop.containerise( name, namespace, layer, context, self.__class__.__name__ ) def update(self, container, representation): layer = container.pop("layer") with photoshop.maintained_selection(): stub.replace_smart_object( layer, get_representation_path(representation) ) stub.imprint( layer, {"representation": str(representation["_id"])} ) def remove(self, container): container["layer"].Delete() def switch(self, container, representation): self.update(container, representation) ``` For easier debugging of Javascript: https://community.adobe.com/t5/download-install/adobe-extension-debuger-problem/td-p/10911704?page=1 Add --enable-blink-features=ShadowDOMV0,CustomElementsV0 when starting Chrome then localhost:8078 (port set in `photoshop\extension\.debug`) Or use Visual Studio Code https://medium.com/adobetech/extendscript-debugger-for-visual-studio-code-public-release-a2ff6161fa01 Or install CEF client from https://github.com/Adobe-CEP/CEP-Resources/tree/master/CEP_9.x ## Resources - https://github.com/lohriialo/photoshop-scripting-python - https://www.adobe.com/devnet/photoshop/scripting.html - https://github.com/Adobe-CEP/Getting-Started-guides - https://github.com/Adobe-CEP/CEP-Resources ================================================ FILE: openpype/hosts/photoshop/api/__init__.py ================================================ """Public API Anything that isn't defined here is INTERNAL and unreliable for external use. """ from .launch_logic import stub from .pipeline import ( PhotoshopHost, ls, containerise ) from .plugin import ( PhotoshopLoader, get_unique_layer_name ) from .lib import ( maintained_selection, maintained_visibility ) __all__ = [ # launch_logic "stub", # pipeline "PhotoshopHost", "ls", "containerise", # Plugin "PhotoshopLoader", "get_unique_layer_name", # lib "maintained_selection", "maintained_visibility", ] ================================================ FILE: openpype/hosts/photoshop/api/extension/.debug ================================================================================================ FILE: openpype/hosts/photoshop/api/extension/CSXS/manifest.xml ================================================ ================================================ FILE: openpype/hosts/photoshop/api/extension/client/CSInterface.js ================================================ /************************************************************************************************** * * ADOBE SYSTEMS INCORPORATED * Copyright 2013 Adobe Systems Incorporated * All Rights Reserved. * * NOTICE: Adobe permits you to use, modify, and distribute this file in accordance with the * terms of the Adobe license agreement accompanying it. If you have received this file from a * source other than Adobe, then your use, modification, or distribution of it requires the prior * written permission of Adobe. * **************************************************************************************************/ /** CSInterface - v8.0.0 */ /** * Stores constants for the window types supported by the CSXS infrastructure. */ function CSXSWindowType() { } /** Constant for the CSXS window type Panel. */ CSXSWindowType._PANEL = "Panel"; /** Constant for the CSXS window type Modeless. */ CSXSWindowType._MODELESS = "Modeless"; /** Constant for the CSXS window type ModalDialog. */ CSXSWindowType._MODAL_DIALOG = "ModalDialog"; /** EvalScript error message */ EvalScript_ErrMessage = "EvalScript error."; /** * @class Version * Defines a version number with major, minor, micro, and special * components. The major, minor and micro values are numeric; the special * value can be any string. * * @param major The major version component, a positive integer up to nine digits long. * @param minor The minor version component, a positive integer up to nine digits long. * @param micro The micro version component, a positive integer up to nine digits long. * @param special The special version component, an arbitrary string. * * @return A new \c Version object. */ function Version(major, minor, micro, special) { this.major = major; this.minor = minor; this.micro = micro; this.special = special; } /** * The maximum value allowed for a numeric version component. * This reflects the maximum value allowed in PlugPlug and the manifest schema. */ Version.MAX_NUM = 999999999; /** * @class VersionBound * Defines a boundary for a version range, which associates a \c Version object * with a flag for whether it is an inclusive or exclusive boundary. * * @param version The \c #Version object. * @param inclusive True if this boundary is inclusive, false if it is exclusive. * * @return A new \c VersionBound object. */ function VersionBound(version, inclusive) { this.version = version; this.inclusive = inclusive; } /** * @class VersionRange * Defines a range of versions using a lower boundary and optional upper boundary. * * @param lowerBound The \c #VersionBound object. * @param upperBound The \c #VersionBound object, or null for a range with no upper boundary. * * @return A new \c VersionRange object. */ function VersionRange(lowerBound, upperBound) { this.lowerBound = lowerBound; this.upperBound = upperBound; } /** * @class Runtime * Represents a runtime related to the CEP infrastructure. * Extensions can declare dependencies on particular * CEP runtime versions in the extension manifest. * * @param name The runtime name. * @param version A \c #VersionRange object that defines a range of valid versions. * * @return A new \c Runtime object. */ function Runtime(name, versionRange) { this.name = name; this.versionRange = versionRange; } /** * @class Extension * Encapsulates a CEP-based extension to an Adobe application. * * @param id The unique identifier of this extension. * @param name The localizable display name of this extension. * @param mainPath The path of the "index.html" file. * @param basePath The base path of this extension. * @param windowType The window type of the main window of this extension. Valid values are defined by \c #CSXSWindowType. * @param width The default width in pixels of the main window of this extension. * @param height The default height in pixels of the main window of this extension. * @param minWidth The minimum width in pixels of the main window of this extension. * @param minHeight The minimum height in pixels of the main window of this extension. * @param maxWidth The maximum width in pixels of the main window of this extension. * @param maxHeight The maximum height in pixels of the main window of this extension. * @param defaultExtensionDataXml The extension data contained in the default \c ExtensionDispatchInfo section of the extension manifest. * @param specialExtensionDataXml The extension data contained in the application-specific \c ExtensionDispatchInfo section of the extension manifest. * @param requiredRuntimeList An array of \c Runtime objects for runtimes required by this extension. * @param isAutoVisible True if this extension is visible on loading. * @param isPluginExtension True if this extension has been deployed in the Plugins folder of the host application. * * @return A new \c Extension object. */ function Extension(id, name, mainPath, basePath, windowType, width, height, minWidth, minHeight, maxWidth, maxHeight, defaultExtensionDataXml, specialExtensionDataXml, requiredRuntimeList, isAutoVisible, isPluginExtension) { this.id = id; this.name = name; this.mainPath = mainPath; this.basePath = basePath; this.windowType = windowType; this.width = width; this.height = height; this.minWidth = minWidth; this.minHeight = minHeight; this.maxWidth = maxWidth; this.maxHeight = maxHeight; this.defaultExtensionDataXml = defaultExtensionDataXml; this.specialExtensionDataXml = specialExtensionDataXml; this.requiredRuntimeList = requiredRuntimeList; this.isAutoVisible = isAutoVisible; this.isPluginExtension = isPluginExtension; } /** * @class CSEvent * A standard JavaScript event, the base class for CEP events. * * @param type The name of the event type. * @param scope The scope of event, can be "GLOBAL" or "APPLICATION". * @param appId The unique identifier of the application that generated the event. * @param extensionId The unique identifier of the extension that generated the event. * * @return A new \c CSEvent object */ function CSEvent(type, scope, appId, extensionId) { this.type = type; this.scope = scope; this.appId = appId; this.extensionId = extensionId; } /** Event-specific data. */ CSEvent.prototype.data = ""; /** * @class SystemPath * Stores operating-system-specific location constants for use in the * \c #CSInterface.getSystemPath() method. * @return A new \c SystemPath object. */ function SystemPath() { } /** The path to user data. */ SystemPath.USER_DATA = "userData"; /** The path to common files for Adobe applications. */ SystemPath.COMMON_FILES = "commonFiles"; /** The path to the user's default document folder. */ SystemPath.MY_DOCUMENTS = "myDocuments"; /** @deprecated. Use \c #SystemPath.Extension. */ SystemPath.APPLICATION = "application"; /** The path to current extension. */ SystemPath.EXTENSION = "extension"; /** The path to hosting application's executable. */ SystemPath.HOST_APPLICATION = "hostApplication"; /** * @class ColorType * Stores color-type constants. */ function ColorType() { } /** RGB color type. */ ColorType.RGB = "rgb"; /** Gradient color type. */ ColorType.GRADIENT = "gradient"; /** Null color type. */ ColorType.NONE = "none"; /** * @class RGBColor * Stores an RGB color with red, green, blue, and alpha values. * All values are in the range [0.0 to 255.0]. Invalid numeric values are * converted to numbers within this range. * * @param red The red value, in the range [0.0 to 255.0]. * @param green The green value, in the range [0.0 to 255.0]. * @param blue The blue value, in the range [0.0 to 255.0]. * @param alpha The alpha (transparency) value, in the range [0.0 to 255.0]. * The default, 255.0, means that the color is fully opaque. * * @return A new RGBColor object. */ function RGBColor(red, green, blue, alpha) { this.red = red; this.green = green; this.blue = blue; this.alpha = alpha; } /** * @class Direction * A point value in which the y component is 0 and the x component * is positive or negative for a right or left direction, * or the x component is 0 and the y component is positive or negative for * an up or down direction. * * @param x The horizontal component of the point. * @param y The vertical component of the point. * * @return A new \c Direction object. */ function Direction(x, y) { this.x = x; this.y = y; } /** * @class GradientStop * Stores gradient stop information. * * @param offset The offset of the gradient stop, in the range [0.0 to 1.0]. * @param rgbColor The color of the gradient at this point, an \c #RGBColor object. * * @return GradientStop object. */ function GradientStop(offset, rgbColor) { this.offset = offset; this.rgbColor = rgbColor; } /** * @class GradientColor * Stores gradient color information. * * @param type The gradient type, must be "linear". * @param direction A \c #Direction object for the direction of the gradient (up, down, right, or left). * @param numStops The number of stops in the gradient. * @param gradientStopList An array of \c #GradientStop objects. * * @return A new \c GradientColor object. */ function GradientColor(type, direction, numStops, arrGradientStop) { this.type = type; this.direction = direction; this.numStops = numStops; this.arrGradientStop = arrGradientStop; } /** * @class UIColor * Stores color information, including the type, anti-alias level, and specific color * values in a color object of an appropriate type. * * @param type The color type, 1 for "rgb" and 2 for "gradient". The supplied color object must correspond to this type. * @param antialiasLevel The anti-alias level constant. * @param color A \c #RGBColor or \c #GradientColor object containing specific color information. * * @return A new \c UIColor object. */ function UIColor(type, antialiasLevel, color) { this.type = type; this.antialiasLevel = antialiasLevel; this.color = color; } /** * @class AppSkinInfo * Stores window-skin properties, such as color and font. All color parameter values are \c #UIColor objects except that systemHighlightColor is \c #RGBColor object. * * @param baseFontFamily The base font family of the application. * @param baseFontSize The base font size of the application. * @param appBarBackgroundColor The application bar background color. * @param panelBackgroundColor The background color of the extension panel. * @param appBarBackgroundColorSRGB The application bar background color, as sRGB. * @param panelBackgroundColorSRGB The background color of the extension panel, as sRGB. * @param systemHighlightColor The highlight color of the extension panel, if provided by the host application. Otherwise, the operating-system highlight color. * * @return AppSkinInfo object. */ function AppSkinInfo(baseFontFamily, baseFontSize, appBarBackgroundColor, panelBackgroundColor, appBarBackgroundColorSRGB, panelBackgroundColorSRGB, systemHighlightColor) { this.baseFontFamily = baseFontFamily; this.baseFontSize = baseFontSize; this.appBarBackgroundColor = appBarBackgroundColor; this.panelBackgroundColor = panelBackgroundColor; this.appBarBackgroundColorSRGB = appBarBackgroundColorSRGB; this.panelBackgroundColorSRGB = panelBackgroundColorSRGB; this.systemHighlightColor = systemHighlightColor; } /** * @class HostEnvironment * Stores information about the environment in which the extension is loaded. * * @param appName The application's name. * @param appVersion The application's version. * @param appLocale The application's current license locale. * @param appUILocale The application's current UI locale. * @param appId The application's unique identifier. * @param isAppOnline True if the application is currently online. * @param appSkinInfo An \c #AppSkinInfo object containing the application's default color and font styles. * * @return A new \c HostEnvironment object. */ function HostEnvironment(appName, appVersion, appLocale, appUILocale, appId, isAppOnline, appSkinInfo) { this.appName = appName; this.appVersion = appVersion; this.appLocale = appLocale; this.appUILocale = appUILocale; this.appId = appId; this.isAppOnline = isAppOnline; this.appSkinInfo = appSkinInfo; } /** * @class HostCapabilities * Stores information about the host capabilities. * * @param EXTENDED_PANEL_MENU True if the application supports panel menu. * @param EXTENDED_PANEL_ICONS True if the application supports panel icon. * @param DELEGATE_APE_ENGINE True if the application supports delegated APE engine. * @param SUPPORT_HTML_EXTENSIONS True if the application supports HTML extensions. * @param DISABLE_FLASH_EXTENSIONS True if the application disables FLASH extensions. * * @return A new \c HostCapabilities object. */ function HostCapabilities(EXTENDED_PANEL_MENU, EXTENDED_PANEL_ICONS, DELEGATE_APE_ENGINE, SUPPORT_HTML_EXTENSIONS, DISABLE_FLASH_EXTENSIONS) { this.EXTENDED_PANEL_MENU = EXTENDED_PANEL_MENU; this.EXTENDED_PANEL_ICONS = EXTENDED_PANEL_ICONS; this.DELEGATE_APE_ENGINE = DELEGATE_APE_ENGINE; this.SUPPORT_HTML_EXTENSIONS = SUPPORT_HTML_EXTENSIONS; this.DISABLE_FLASH_EXTENSIONS = DISABLE_FLASH_EXTENSIONS; // Since 5.0.0 } /** * @class ApiVersion * Stores current api version. * * Since 4.2.0 * * @param major The major version * @param minor The minor version. * @param micro The micro version. * * @return ApiVersion object. */ function ApiVersion(major, minor, micro) { this.major = major; this.minor = minor; this.micro = micro; } /** * @class MenuItemStatus * Stores flyout menu item status * * Since 5.2.0 * * @param menuItemLabel The menu item label. * @param enabled True if user wants to enable the menu item. * @param checked True if user wants to check the menu item. * * @return MenuItemStatus object. */ function MenuItemStatus(menuItemLabel, enabled, checked) { this.menuItemLabel = menuItemLabel; this.enabled = enabled; this.checked = checked; } /** * @class ContextMenuItemStatus * Stores the status of the context menu item. * * Since 5.2.0 * * @param menuItemID The menu item id. * @param enabled True if user wants to enable the menu item. * @param checked True if user wants to check the menu item. * * @return MenuItemStatus object. */ function ContextMenuItemStatus(menuItemID, enabled, checked) { this.menuItemID = menuItemID; this.enabled = enabled; this.checked = checked; } //------------------------------ CSInterface ---------------------------------- /** * @class CSInterface * This is the entry point to the CEP extensibility infrastructure. * Instantiate this object and use it to: * ./index.html true applicationActivate com.adobe.csxs.events.ApplicationInitialized Panel 300 140 400 200 ./icons/ayon_logo.png *
* * @return A new \c CSInterface object */ function CSInterface() { } /** * User can add this event listener to handle native application theme color changes. * Callback function gives extensions ability to fine-tune their theme color after the * global theme color has been changed. * The callback function should be like below: * * @example * // event is a CSEvent object, but user can ignore it. * function OnAppThemeColorChanged(event) * { * // Should get a latest HostEnvironment object from application. * var skinInfo = JSON.parse(window.__adobe_cep__.getHostEnvironment()).appSkinInfo; * // Gets the style information such as color info from the skinInfo, * // and redraw all UI controls of your extension according to the style info. * } */ CSInterface.THEME_COLOR_CHANGED_EVENT = "com.adobe.csxs.events.ThemeColorChanged"; /** The host environment data object. */ CSInterface.prototype.hostEnvironment = window.__adobe_cep__ ? JSON.parse(window.__adobe_cep__.getHostEnvironment()) : null; /** Retrieves information about the host environment in which the * extension is currently running. * * @return A \c #HostEnvironment object. */ CSInterface.prototype.getHostEnvironment = function() { this.hostEnvironment = JSON.parse(window.__adobe_cep__.getHostEnvironment()); return this.hostEnvironment; }; /** Closes this extension. */ CSInterface.prototype.closeExtension = function() { window.__adobe_cep__.closeExtension(); }; /** * Retrieves a path for which a constant is defined in the system. * * @param pathType The path-type constant defined in \c #SystemPath , * * @return The platform-specific system path string. */ CSInterface.prototype.getSystemPath = function(pathType) { var path = decodeURI(window.__adobe_cep__.getSystemPath(pathType)); var OSVersion = this.getOSInformation(); if (OSVersion.indexOf("Windows") >= 0) { path = path.replace("file:///", ""); } else if (OSVersion.indexOf("Mac") >= 0) { path = path.replace("file://", ""); } return path; }; /** * Evaluates a JavaScript script, which can use the JavaScript DOM * of the host application. * * @param script The JavaScript script. * @param callback Optional. A callback function that receives the result of execution. * If execution fails, the callback function receives the error message \c EvalScript_ErrMessage. */ CSInterface.prototype.evalScript = function(script, callback) { if(callback === null || callback === undefined) { callback = function(result){}; } window.__adobe_cep__.evalScript(script, callback); }; /** * Retrieves the unique identifier of the application. * in which the extension is currently running. * * @return The unique ID string. */ CSInterface.prototype.getApplicationID = function() { var appId = this.hostEnvironment.appId; return appId; }; /** * Retrieves host capability information for the application * in which the extension is currently running. * * @return A \c #HostCapabilities object. */ CSInterface.prototype.getHostCapabilities = function() { var hostCapabilities = JSON.parse(window.__adobe_cep__.getHostCapabilities() ); return hostCapabilities; }; /** * Triggers a CEP event programmatically. Yoy can use it to dispatch * an event of a predefined type, or of a type you have defined. * * @param event A \c CSEvent object. */ CSInterface.prototype.dispatchEvent = function(event) { if (typeof event.data == "object") { event.data = JSON.stringify(event.data); } window.__adobe_cep__.dispatchEvent(event); }; /** * Registers an interest in a CEP event of a particular type, and * assigns an event handler. * The event infrastructure notifies your extension when events of this type occur, * passing the event object to the registered handler function. * * @param type The name of the event type of interest. * @param listener The JavaScript handler function or method. * @param obj Optional, the object containing the handler method, if any. * Default is null. */ CSInterface.prototype.addEventListener = function(type, listener, obj) { window.__adobe_cep__.addEventListener(type, listener, obj); }; /** * Removes a registered event listener. * * @param type The name of the event type of interest. * @param listener The JavaScript handler function or method that was registered. * @param obj Optional, the object containing the handler method, if any. * Default is null. */ CSInterface.prototype.removeEventListener = function(type, listener, obj) { window.__adobe_cep__.removeEventListener(type, listener, obj); }; /** * Loads and launches another extension, or activates the extension if it is already loaded. * * @param extensionId The extension's unique identifier. * @param startupParams Not currently used, pass "". * * @example * To launch the extension "help" with ID "HLP" from this extension, call: *- Access information about the host application in which an extension is running
*- Launch an extension
*- Register interest in event notifications, and dispatch events
*requestOpenExtension("HLP", "");* */ CSInterface.prototype.requestOpenExtension = function(extensionId, params) { window.__adobe_cep__.requestOpenExtension(extensionId, params); }; /** * Retrieves the list of extensions currently loaded in the current host application. * The extension list is initialized once, and remains the same during the lifetime * of the CEP session. * * @param extensionIds Optional, an array of unique identifiers for extensions of interest. * If omitted, retrieves data for all extensions. * * @return Zero or more \c #Extension objects. */ CSInterface.prototype.getExtensions = function(extensionIds) { var extensionIdsStr = JSON.stringify(extensionIds); var extensionsStr = window.__adobe_cep__.getExtensions(extensionIdsStr); var extensions = JSON.parse(extensionsStr); return extensions; }; /** * Retrieves network-related preferences. * * @return A JavaScript object containing network preferences. */ CSInterface.prototype.getNetworkPreferences = function() { var result = window.__adobe_cep__.getNetworkPreferences(); var networkPre = JSON.parse(result); return networkPre; }; /** * Initializes the resource bundle for this extension with property values * for the current application and locale. * To support multiple locales, you must define a property file for each locale, * containing keyed display-string values for that locale. * See localization documentation for Extension Builder and related products. * * Keys can be in the * formkey.value="localized string", for use in HTML text elements. * For example, in this input element, the localized \c key.value string is displayed * instead of the empty \c value string: * ** * @return An object containing the resource bundle information. */ CSInterface.prototype.initResourceBundle = function() { var resourceBundle = JSON.parse(window.__adobe_cep__.initResourceBundle()); var resElms = document.querySelectorAll('[data-locale]'); for (var n = 0; n < resElms.length; n++) { var resEl = resElms[n]; // Get the resource key from the element. var resKey = resEl.getAttribute('data-locale'); if (resKey) { // Get all the resources that start with the key. for (var key in resourceBundle) { if (key.indexOf(resKey) === 0) { var resValue = resourceBundle[key]; if (key.length == resKey.length) { resEl.innerHTML = resValue; } else if ('.' == key.charAt(resKey.length)) { var attrKey = key.substring(resKey.length + 1); resEl[attrKey] = resValue; } } } } } return resourceBundle; }; /** * Writes installation information to a file. * * @return The file path. */ CSInterface.prototype.dumpInstallationInfo = function() { return window.__adobe_cep__.dumpInstallationInfo(); }; /** * Retrieves version information for the current Operating System, * See http://www.useragentstring.com/pages/Chrome/ for Chrome \c navigator.userAgent values. * * @return A string containing the OS version, or "unknown Operation System". * If user customizes the User Agent by setting CEF command parameter "--user-agent", only * "Mac OS X" or "Windows" will be returned. */ CSInterface.prototype.getOSInformation = function() { var userAgent = navigator.userAgent; if ((navigator.platform == "Win32") || (navigator.platform == "Windows")) { var winVersion = "Windows"; var winBit = ""; if (userAgent.indexOf("Windows") > -1) { if (userAgent.indexOf("Windows NT 5.0") > -1) { winVersion = "Windows 2000"; } else if (userAgent.indexOf("Windows NT 5.1") > -1) { winVersion = "Windows XP"; } else if (userAgent.indexOf("Windows NT 5.2") > -1) { winVersion = "Windows Server 2003"; } else if (userAgent.indexOf("Windows NT 6.0") > -1) { winVersion = "Windows Vista"; } else if (userAgent.indexOf("Windows NT 6.1") > -1) { winVersion = "Windows 7"; } else if (userAgent.indexOf("Windows NT 6.2") > -1) { winVersion = "Windows 8"; } else if (userAgent.indexOf("Windows NT 6.3") > -1) { winVersion = "Windows 8.1"; } else if (userAgent.indexOf("Windows NT 10") > -1) { winVersion = "Windows 10"; } if (userAgent.indexOf("WOW64") > -1 || userAgent.indexOf("Win64") > -1) { winBit = " 64-bit"; } else { winBit = " 32-bit"; } } return winVersion + winBit; } else if ((navigator.platform == "MacIntel") || (navigator.platform == "Macintosh")) { var result = "Mac OS X"; if (userAgent.indexOf("Mac OS X") > -1) { result = userAgent.substring(userAgent.indexOf("Mac OS X"), userAgent.indexOf(")")); result = result.replace(/_/g, "."); } return result; } return "Unknown Operation System"; }; /** * Opens a page in the default system browser. * * Since 4.2.0 * * @param url The URL of the page/file to open, or the email address. * Must use HTTP/HTTPS/file/mailto protocol. For example: * "http://www.adobe.com" * "https://github.com" * "file:///C:/log.txt" * "mailto:test@adobe.com" * * @return One of these error codes:\n *\n *
\n */ CSInterface.prototype.openURLInDefaultBrowser = function(url) { return cep.util.openURLInDefaultBrowser(url); }; /** * Retrieves extension ID. * * Since 4.2.0 * * @return extension ID. */ CSInterface.prototype.getExtensionID = function() { return window.__adobe_cep__.getExtensionId(); }; /** * Retrieves the scale factor of screen. * On Windows platform, the value of scale factor might be different from operating system's scale factor, * since host application may use its self-defined scale factor. * * Since 4.2.0 * * @return One of the following float number. *- NO_ERROR - 0
\n *- ERR_UNKNOWN - 1
\n *- ERR_INVALID_PARAMS - 2
\n *- ERR_INVALID_URL - 201
\n *\n *
\n */ CSInterface.prototype.getScaleFactor = function() { return window.__adobe_cep__.getScaleFactor(); }; /** * Set a handler to detect any changes of scale factor. This only works on Mac. * * Since 4.2.0 * * @param handler The function to be called when scale factor is changed. * */ CSInterface.prototype.setScaleFactorChangedHandler = function(handler) { window.__adobe_cep__.setScaleFactorChangedHandler(handler); }; /** * Retrieves current API version. * * Since 4.2.0 * * @return ApiVersion object. * */ CSInterface.prototype.getCurrentApiVersion = function() { var apiVersion = JSON.parse(window.__adobe_cep__.getCurrentApiVersion()); return apiVersion; }; /** * Set panel flyout menu by an XML. * * Since 5.2.0 * * Register a callback function for "com.adobe.csxs.events.flyoutMenuClicked" to get notified when a * menu item is clicked. * The "data" attribute of event is an object which contains "menuId" and "menuName" attributes. * * Register callback functions for "com.adobe.csxs.events.flyoutMenuOpened" and "com.adobe.csxs.events.flyoutMenuClosed" * respectively to get notified when flyout menu is opened or closed. * * @param menu A XML string which describes menu structure. * An example menu XML: * * */ CSInterface.prototype.setPanelFlyoutMenu = function(menu) { if ("string" != typeof menu) { return; } window.__adobe_cep__.invokeSync("setPanelFlyoutMenu", menu); }; /** * Updates a menu item in the extension window's flyout menu, by setting the enabled * and selection status. * * Since 5.2.0 * * @param menuItemLabel The menu item label. * @param enabled True to enable the item, false to disable it (gray it out). * @param checked True to select the item, false to deselect it. * * @return false when the host application does not support this functionality (HostCapabilities.EXTENDED_PANEL_MENU is false). * Fails silently if menu label is invalid. * * @see HostCapabilities.EXTENDED_PANEL_MENU */ CSInterface.prototype.updatePanelMenuItem = function(menuItemLabel, enabled, checked) { var ret = false; if (this.getHostCapabilities().EXTENDED_PANEL_MENU) { var itemStatus = new MenuItemStatus(menuItemLabel, enabled, checked); ret = window.__adobe_cep__.invokeSync("updatePanelMenuItem", JSON.stringify(itemStatus)); } return ret; }; /** * Set context menu by XML string. * * Since 5.2.0 * * There are a number of conventions used to communicate what type of menu item to create and how it should be handled. * - an item without menu ID or menu name is disabled and is not shown. * - if the item name is "---" (three hyphens) then it is treated as a separator. The menu ID in this case will always be NULL. * - Checkable attribute takes precedence over Checked attribute. * - a PNG icon. For optimal display results please supply a 16 x 16px icon as larger dimensions will increase the size of the menu item. The Chrome extension contextMenus API was taken as a reference. https://developer.chrome.com/extensions/contextMenus * - the items with icons and checkable items cannot coexist on the same menu level. The former take precedences over the latter. * * @param menu A XML string which describes menu structure. * @param callback The callback function which is called when a menu item is clicked. The only parameter is the returned ID of clicked menu item. * * @description An example menu XML: * */ CSInterface.prototype.setContextMenu = function(menu, callback) { if ("string" != typeof menu) { return; } window.__adobe_cep__.invokeAsync("setContextMenu", menu, callback); }; /** * Set context menu by JSON string. * * Since 6.0.0 * * There are a number of conventions used to communicate what type of menu item to create and how it should be handled. * - an item without menu ID or menu name is disabled and is not shown. * - if the item label is "---" (three hyphens) then it is treated as a separator. The menu ID in this case will always be NULL. * - Checkable attribute takes precedence over Checked attribute. * - a PNG icon. For optimal display results please supply a 16 x 16px icon as larger dimensions will increase the size of the menu item. The Chrome extension contextMenus API was taken as a reference. * - the items with icons and checkable items cannot coexist on the same menu level. The former take precedences over the latter. https://developer.chrome.com/extensions/contextMenus * * @param menu A JSON string which describes menu structure. * @param callback The callback function which is called when a menu item is clicked. The only parameter is the returned ID of clicked menu item. * * @description An example menu JSON: * * { * "menu": [ * { * "id": "menuItemId1", * "label": "testExample1", * "enabled": true, * "checkable": true, * "checked": false, * "icon": "./image/small_16X16.png" * }, * { * "id": "menuItemId2", * "label": "testExample2", * "menu": [ * { * "id": "menuItemId2-1", * "label": "testExample2-1", * "menu": [ * { * "id": "menuItemId2-1-1", * "label": "testExample2-1-1", * "enabled": false, * "checkable": true, * "checked": true * } * ] * }, * { * "id": "menuItemId2-2", * "label": "testExample2-2", * "enabled": true, * "checkable": true, * "checked": true * } * ] * }, * { * "label": "---" * }, * { * "id": "menuItemId3", * "label": "testExample3", * "enabled": false, * "checkable": true, * "checked": false * } * ] * } * */ CSInterface.prototype.setContextMenuByJSON = function(menu, callback) { if ("string" != typeof menu) { return; } window.__adobe_cep__.invokeAsync("setContextMenuByJSON", menu, callback); }; /** * Updates a context menu item by setting the enabled and selection status. * * Since 5.2.0 * * @param menuItemID The menu item ID. * @param enabled True to enable the item, false to disable it (gray it out). * @param checked True to select the item, false to deselect it. */ CSInterface.prototype.updateContextMenuItem = function(menuItemID, enabled, checked) { var itemStatus = new ContextMenuItemStatus(menuItemID, enabled, checked); ret = window.__adobe_cep__.invokeSync("updateContextMenuItem", JSON.stringify(itemStatus)); }; /** * Get the visibility status of an extension window. * * Since 6.0.0 * * @return true if the extension window is visible; false if the extension window is hidden. */ CSInterface.prototype.isWindowVisible = function() { return window.__adobe_cep__.invokeSync("isWindowVisible", ""); }; /** * Resize extension's content to the specified dimensions. * 1. Works with modal and modeless extensions in all Adobe products. * 2. Extension's manifest min/max size constraints apply and take precedence. * 3. For panel extensions * 3.1 This works in all Adobe products except: * * Premiere Pro * * Prelude * * After Effects * 3.2 When the panel is in certain states (especially when being docked), * it will not change to the desired dimensions even when the * specified size satisfies min/max constraints. * * Since 6.0.0 * * @param width The new width * @param height The new height */ CSInterface.prototype.resizeContent = function(width, height) { window.__adobe_cep__.resizeContent(width, height); }; /** * Register the invalid certificate callback for an extension. * This callback will be triggered when the extension tries to access the web site that contains the invalid certificate on the main frame. * But if the extension does not call this function and tries to access the web site containing the invalid certificate, a default error page will be shown. * * Since 6.1.0 * * @param callback the callback function */ CSInterface.prototype.registerInvalidCertificateCallback = function(callback) { return window.__adobe_cep__.registerInvalidCertificateCallback(callback); }; /** * Register an interest in some key events to prevent them from being sent to the host application. * * This function works with modeless extensions and panel extensions. * Generally all the key events will be sent to the host application for these two extensions if the current focused element * is not text input or dropdown, * If you want to intercept some key events and want them to be handled in the extension, please call this function * in advance to prevent them being sent to the host application. * * Since 6.1.0 * * @param keyEventsInterest A JSON string describing those key events you are interested in. A null object or an empty string will lead to removing the interest * * This JSON string should be an array, each object has following keys: * * keyCode: [Required] represents an OS system dependent virtual key code identifying * the unmodified value of the pressed key. * ctrlKey: [optional] a Boolean that indicates if the control key was pressed (true) or not (false) when the event occurred. * altKey: [optional] a Boolean that indicates if the alt key was pressed (true) or not (false) when the event occurred. * shiftKey: [optional] a Boolean that indicates if the shift key was pressed (true) or not (false) when the event occurred. * metaKey: [optional] (Mac Only) a Boolean that indicates if the Meta key was pressed (true) or not (false) when the event occurred. * On Macintosh keyboards, this is the command key. To detect Windows key on Windows, please use keyCode instead. * An example JSON string: * * [ * { * "keyCode": 48 * }, * { * "keyCode": 123, * "ctrlKey": true * }, * { * "keyCode": 123, * "ctrlKey": true, * "metaKey": true * } * ] * */ CSInterface.prototype.registerKeyEventsInterest = function(keyEventsInterest) { return window.__adobe_cep__.registerKeyEventsInterest(keyEventsInterest); }; /** * Set the title of the extension window. * This function works with modal and modeless extensions in all Adobe products, and panel extensions in Photoshop, InDesign, InCopy, Illustrator, Flash Pro and Dreamweaver. * * Since 6.1.0 * * @param title The window title. */ CSInterface.prototype.setWindowTitle = function(title) { window.__adobe_cep__.invokeSync("setWindowTitle", title); }; /** * Get the title of the extension window. * This function works with modal and modeless extensions in all Adobe products, and panel extensions in Photoshop, InDesign, InCopy, Illustrator, Flash Pro and Dreamweaver. * * Since 6.1.0 * * @return The window title. */ CSInterface.prototype.getWindowTitle = function() { return window.__adobe_cep__.invokeSync("getWindowTitle", ""); }; ================================================ FILE: openpype/hosts/photoshop/api/extension/client/client.js ================================================ // client facing part of extension, creates WSRPC client (jsx cannot // do that) // consumes RPC calls from server (OpenPype) calls ./host/index.jsx and // returns values back (in json format) var logReturn = function(result){ log.warn('Result: ' + result);}; var csInterface = new CSInterface(); log.warn("script start"); WSRPC.DEBUG = false; WSRPC.TRACE = false; function myCallBack(){ log.warn("Triggered index.jsx"); } // importing through manifest.xml isn't working because relative paths // possibly TODO jsx.evalFile('./host/index.jsx', myCallBack); function runEvalScript(script) { // because of asynchronous nature of functions in jsx // this waits for response return new Promise(function(resolve, reject){ csInterface.evalScript(script, resolve); }); } /** main entry point **/ startUp("WEBSOCKET_URL"); // get websocket server url from environment value async function startUp(url){ log.warn("url", url); promis = runEvalScript("getEnv('" + url + "')"); var res = await promis; // run rest only after resolved promise main(res); } function get_extension_version(){ /** Returns version number from extension manifest.xml **/ log.debug("get_extension_version") var path = csInterface.getSystemPath(SystemPath.EXTENSION); log.debug("extension path " + path); var result = window.cep.fs.readFile(path + "/CSXS/manifest.xml"); var version = undefined; if(result.err === 0){ if (window.DOMParser) { const parser = new DOMParser(); const xmlDoc = parser.parseFromString(result.data.toString(), 'text/xml'); const children = xmlDoc.children; for (let i = 0; i <= children.length; i++) { if (children[i] && children[i].getAttribute('ExtensionBundleVersion')) { version = children[i].getAttribute('ExtensionBundleVersion'); } } } } return version } function main(websocket_url){ // creates connection to 'websocket_url', registers routes log.warn("websocket_url", websocket_url); var default_url = 'ws://localhost:8099/ws/'; if (websocket_url == ''){ websocket_url = default_url; } log.warn("connecting to:", websocket_url); RPC = new WSRPC(websocket_url, 5000); // spin connection RPC.connect(); log.warn("connected"); function EscapeStringForJSX(str){ // Replaces: // \ with \\ // ' with \' // " with \" // See: https://stackoverflow.com/a/3967927/5285364 return str.replace(/\\/g, '\\\\').replace(/'/g, "\\'").replace(/"/g, '\\"'); } RPC.addRoute('Photoshop.open', function (data) { log.warn('Server called client route "open":', data); var escapedPath = EscapeStringForJSX(data.path); return runEvalScript("fileOpen('" + escapedPath +"')") .then(function(result){ log.warn("open: " + result); return result; }); }); RPC.addRoute('Photoshop.read', function (data) { log.warn('Server called client route "read":', data); return runEvalScript("getHeadline()") .then(function(result){ log.warn("getHeadline: " + result); return result; }); }); RPC.addRoute('Photoshop.get_layers', function (data) { log.warn('Server called client route "get_layers":', data); return runEvalScript("getLayers()") .then(function(result){ log.warn("getLayers: " + result); return result; }); }); RPC.addRoute('Photoshop.set_visible', function (data) { log.warn('Server called client route "set_visible":', data); return runEvalScript("setVisible(" + data.layer_id + ", " + data.visibility + ")") .then(function(result){ log.warn("setVisible: " + result); return result; }); }); RPC.addRoute('Photoshop.get_active_document_name', function (data) { log.warn('Server called client route "get_active_document_name":', data); return runEvalScript("getActiveDocumentName()") .then(function(result){ log.warn("save: " + result); return result; }); }); RPC.addRoute('Photoshop.get_active_document_full_name', function (data) { log.warn('Server called client route ' + '"get_active_document_full_name":', data); return runEvalScript("getActiveDocumentFullName()") .then(function(result){ log.warn("save: " + result); return result; }); }); RPC.addRoute('Photoshop.save', function (data) { log.warn('Server called client route "save":', data); return runEvalScript("save()") .then(function(result){ log.warn("save: " + result); return result; }); }); RPC.addRoute('Photoshop.get_selected_layers', function (data) { log.warn('Server called client route "get_selected_layers":', data); return runEvalScript("getSelectedLayers()") .then(function(result){ log.warn("get_selected_layers: " + result); return result; }); }); RPC.addRoute('Photoshop.create_group', function (data) { log.warn('Server called client route "create_group":', data); return runEvalScript("createGroup('" + data.name + "')") .then(function(result){ log.warn("createGroup: " + result); return result; }); }); RPC.addRoute('Photoshop.group_selected_layers', function (data) { log.warn('Server called client route "group_selected_layers":', data); return runEvalScript("groupSelectedLayers(null, "+ "'" + data.name +"')") .then(function(result){ log.warn("group_selected_layers: " + result); return result; }); }); RPC.addRoute('Photoshop.import_smart_object', function (data) { log.warn('Server called client "import_smart_object":', data); var escapedPath = EscapeStringForJSX(data.path); return runEvalScript("importSmartObject('" + escapedPath +"', " + "'"+ data.name +"',"+ + data.as_reference +")") .then(function(result){ log.warn("import_smart_object: " + result); return result; }); }); RPC.addRoute('Photoshop.replace_smart_object', function (data) { log.warn('Server called route "replace_smart_object":', data); var escapedPath = EscapeStringForJSX(data.path); return runEvalScript("replaceSmartObjects("+data.layer_id+"," + "'" + escapedPath +"',"+ "'"+ data.name +"')") .then(function(result){ log.warn("replaceSmartObjects: " + result); return result; }); }); RPC.addRoute('Photoshop.delete_layer', function (data) { log.warn('Server called route "delete_layer":', data); return runEvalScript("deleteLayer("+data.layer_id+")") .then(function(result){ log.warn("delete_layer: " + result); return result; }); }); RPC.addRoute('Photoshop.rename_layer', function (data) { log.warn('Server called route "rename_layer":', data); return runEvalScript("renameLayer("+data.layer_id+", " + "'"+ data.name +"')") .then(function(result){ log.warn("rename_layer: " + result); return result; }); }); RPC.addRoute('Photoshop.select_layers', function (data) { log.warn('Server called client route "select_layers":', data); return runEvalScript("selectLayers('" + data.layers +"')") .then(function(result){ log.warn("select_layers: " + result); return result; }); }); RPC.addRoute('Photoshop.is_saved', function (data) { log.warn('Server called client route "is_saved":', data); return runEvalScript("isSaved()") .then(function(result){ log.warn("is_saved: " + result); return result; }); }); RPC.addRoute('Photoshop.saveAs', function (data) { log.warn('Server called client route "saveAsJPEG":', data); var escapedPath = EscapeStringForJSX(data.image_path); return runEvalScript("saveAs('" + escapedPath + "', " + "'" + data.ext + "', " + data.as_copy + ")") .then(function(result){ log.warn("save: " + result); return result; }); }); RPC.addRoute('Photoshop.imprint', function (data) { log.warn('Server called client route "imprint":', data); var escaped = data.payload.replace(/\n/g, "\\n"); return runEvalScript("imprint('" + escaped + "')") .then(function(result){ log.warn("imprint: " + result); return result; }); }); RPC.addRoute('Photoshop.get_extension_version', function (data) { log.warn('Server called client route "get_extension_version":', data); return get_extension_version(); }); RPC.addRoute('Photoshop.close', function (data) { log.warn('Server called client route "close":', data); return runEvalScript("close()"); }); RPC.call('Photoshop.ping').then(function (data) { log.warn('Result for calling server route "ping": ', data); return runEvalScript("ping()") .then(function(result){ log.warn("ping: " + result); return result; }); }, function (error) { log.warn(error); }); } log.warn("end script"); ================================================ FILE: openpype/hosts/photoshop/api/extension/client/wsrpc.js ================================================ (function (global, factory) { typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : typeof define === 'function' && define.amd ? define(factory) : (global = global || self, global.WSRPC = factory()); }(this, function () { 'use strict'; function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } var Deferred = function Deferred() { _classCallCheck(this, Deferred); var self = this; self.resolve = null; self.reject = null; self.done = false; function wrapper(func) { return function () { if (self.done) throw new Error('Promise already done'); self.done = true; return func.apply(this, arguments); }; } self.promise = new Promise(function (resolve, reject) { self.resolve = wrapper(resolve); self.reject = wrapper(reject); }); self.promise.isPending = function () { return !self.done; }; return self; }; function logGroup(group, level, args) { console.group(group); console[level].apply(this, args); console.groupEnd(); } function log() { if (!WSRPC.DEBUG) return; logGroup('WSRPC.DEBUG', 'trace', arguments); } function trace(msg) { if (!WSRPC.TRACE) return; var payload = msg; if ('data' in msg) payload = JSON.parse(msg.data); logGroup("WSRPC.TRACE", 'trace', [payload]); } function getAbsoluteWsUrl(url) { if (/^\w+:\/\//.test(url)) return url; if (typeof window == 'undefined' && window.location.host.length < 1) throw new Error("Can not construct absolute URL from ".concat(window.location)); var scheme = window.location.protocol === "https:" ? "wss:" : "ws:"; var port = window.location.port === '' ? ":".concat(window.location.port) : ''; var host = window.location.host; var path = url.replace(/^\/+/gm, ''); return "".concat(scheme, "//").concat(host).concat(port, "/").concat(path); } var readyState = Object.freeze({ 0: 'CONNECTING', 1: 'OPEN', 2: 'CLOSING', 3: 'CLOSED' }); var WSRPC = function WSRPC(URL) { var reconnectTimeout = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 1000; _classCallCheck(this, WSRPC); var self = this; URL = getAbsoluteWsUrl(URL); self.id = 1; self.eventId = 0; self.socketStarted = false; self.eventStore = { onconnect: {}, onerror: {}, onclose: {}, onchange: {} }; self.connectionNumber = 0; self.oneTimeEventStore = { onconnect: [], onerror: [], onclose: [], onchange: [] }; self.callQueue = []; function createSocket() { var ws = new WebSocket(URL); var rejectQueue = function rejectQueue() { self.connectionNumber++; // rejects incoming calls var deferred; //reject all pending calls while (0 < self.callQueue.length) { var callObj = self.callQueue.shift(); deferred = self.store[callObj.id]; delete self.store[callObj.id]; if (deferred && deferred.promise.isPending()) { deferred.reject('WebSocket error occurred'); } } // reject all from the store for (var key in self.store) { if (!self.store.hasOwnProperty(key)) continue; deferred = self.store[key]; if (deferred && deferred.promise.isPending()) { deferred.reject('WebSocket error occurred'); } } }; function reconnect(callEvents) { setTimeout(function () { try { self.socket = createSocket(); self.id = 1; } catch (exc) { callEvents('onerror', exc); delete self.socket; console.error(exc); } }, reconnectTimeout); } ws.onclose = function (err) { log('ONCLOSE CALLED', 'STATE', self.public.state()); trace(err); for (var serial in self.store) { if (!self.store.hasOwnProperty(serial)) continue; if (self.store[serial].hasOwnProperty('reject')) { self.store[serial].reject('Connection closed'); } } rejectQueue(); callEvents('onclose', err); callEvents('onchange', err); reconnect(callEvents); }; ws.onerror = function (err) { log('ONERROR CALLED', 'STATE', self.public.state()); trace(err); rejectQueue(); callEvents('onerror', err); callEvents('onchange', err); log('WebSocket has been closed by error: ', err); }; function tryCallEvent(func, event) { try { return func(event); } catch (e) { if (e.hasOwnProperty('stack')) { log(e.stack); } else { log('Event function', func, 'raised unknown error:', e); } console.error(e); } } function callEvents(evName, event) { while (0 < self.oneTimeEventStore[evName].length) { var deferred = self.oneTimeEventStore[evName].shift(); if (deferred.hasOwnProperty('resolve') && deferred.promise.isPending()) deferred.resolve(); } for (var i in self.eventStore[evName]) { if (!self.eventStore[evName].hasOwnProperty(i)) continue; var cur = self.eventStore[evName][i]; tryCallEvent(cur, event); } } ws.onopen = function (ev) { log('ONOPEN CALLED', 'STATE', self.public.state()); trace(ev); while (0 < self.callQueue.length) { // noinspection JSUnresolvedFunction self.socket.send(JSON.stringify(self.callQueue.shift(), 0, 1)); } callEvents('onconnect', ev); callEvents('onchange', ev); }; function handleCall(self, data) { if (!self.routes.hasOwnProperty(data.method)) throw new Error('Route not found'); var connectionNumber = self.connectionNumber; var deferred = new Deferred(); deferred.promise.then(function (result) { if (connectionNumber !== self.connectionNumber) return; self.socket.send(JSON.stringify({ id: data.id, result: result })); }, function (error) { if (connectionNumber !== self.connectionNumber) return; self.socket.send(JSON.stringify({ id: data.id, error: error })); }); var func = self.routes[data.method]; if (self.asyncRoutes[data.method]) return func.apply(deferred, [data.params]); function badPromise() { throw new Error("You should register route with async flag."); } var promiseMock = { resolve: badPromise, reject: badPromise }; try { deferred.resolve(func.apply(promiseMock, [data.params])); } catch (e) { deferred.reject(e); console.error(e); } } function handleError(self, data) { if (!self.store.hasOwnProperty(data.id)) return log('Unknown callback'); var deferred = self.store[data.id]; if (typeof deferred === 'undefined') return log('Confirmation without handler'); delete self.store[data.id]; log('REJECTING', data.error); deferred.reject(data.error); } function handleResult(self, data) { var deferred = self.store[data.id]; if (typeof deferred === 'undefined') return log('Confirmation without handler'); delete self.store[data.id]; if (data.hasOwnProperty('result')) { return deferred.resolve(data.result); } return deferred.reject(data.error); } ws.onmessage = function (message) { log('ONMESSAGE CALLED', 'STATE', self.public.state()); trace(message); if (message.type !== 'message') return; var data; try { data = JSON.parse(message.data); log(data); if (data.hasOwnProperty('method')) { return handleCall(self, data); } else if (data.hasOwnProperty('error') && data.error === null) { return handleError(self, data); } else { return handleResult(self, data); } } catch (exception) { var err = { error: exception.message, result: null, id: data ? data.id : null }; self.socket.send(JSON.stringify(err)); console.error(exception); } }; return ws; } function makeCall(func, args, params) { self.id += 2; var deferred = new Deferred(); var callObj = Object.freeze({ id: self.id, method: func, params: args }); var state = self.public.state(); if (state === 'OPEN') { self.store[self.id] = deferred; self.socket.send(JSON.stringify(callObj)); } else if (state === 'CONNECTING') { log('SOCKET IS', state); self.store[self.id] = deferred; self.callQueue.push(callObj); } else { log('SOCKET IS', state); if (params && params['noWait']) { deferred.reject("Socket is: ".concat(state)); } else { self.store[self.id] = deferred; self.callQueue.push(callObj); } } return deferred.promise; } self.asyncRoutes = {}; self.routes = {}; self.store = {}; self.public = Object.freeze({ call: function call(func, args, params) { return makeCall(func, args, params); }, addRoute: function addRoute(route, callback, isAsync) { self.asyncRoutes[route] = isAsync || false; self.routes[route] = callback; }, deleteRoute: function deleteRoute(route) { delete self.asyncRoutes[route]; return delete self.routes[route]; }, addEventListener: function addEventListener(event, func) { var eventId = self.eventId++; self.eventStore[event][eventId] = func; return eventId; }, removeEventListener: function removeEventListener(event, index) { if (self.eventStore[event].hasOwnProperty(index)) { delete self.eventStore[event][index]; return true; } else { return false; } }, onEvent: function onEvent(event) { var deferred = new Deferred(); self.oneTimeEventStore[event].push(deferred); return deferred.promise; }, destroy: function destroy() { return self.socket.close(); }, state: function state() { return readyState[this.stateCode()]; }, stateCode: function stateCode() { if (self.socketStarted && self.socket) return self.socket.readyState; return 3; }, connect: function connect() { self.socketStarted = true; self.socket = createSocket(); } }); self.public.addRoute('log', function (argsObj) { //console.info("Websocket sent: ".concat(argsObj)); }); self.public.addRoute('ping', function (data) { return data; }); return self.public; }; WSRPC.DEBUG = false; WSRPC.TRACE = false; return WSRPC; })); //# sourceMappingURL=wsrpc.js.map ================================================ FILE: openpype/hosts/photoshop/api/extension/host/JSX.js ================================================ /* _ ______ __ _ | / ___\ \/ / (_)___ _ | \___ \\ / | / __| | |_| |___) / \ _ | \__ \ \___/|____/_/\_(_)/ |___/ |__/ _ ____ /\ /\___ _ __ ___(_) ___ _ __ |___ \ \ \ / / _ \ '__/ __| |/ _ \| '_ \ __) | \ V / __/ | \__ \ | (_) | | | | / __/ \_/ \___|_| |___/_|\___/|_| |_| |_____| */ ////////////////////////////////////////////////////////////////////////////////// // JSX.js and writtent by Trevor https://creative-scripts.com/jsx-js // // If you turn over is less the $50,000,000 then you don't have to pay anything // // License MIT, don't complain, don't sue NO MATTER WHAT // // If you turn over is more the $50,000,000 then you DO have to pay // // Contact me https://creative-scripts.com/contact for pricing and licensing // // Don't remove these commented lines // // For simple and effective calling of jsx from the js engine // // Version 2 last modified April 18 2018 // ////////////////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////////////////////////////////////// // Change log: // // JSX.js V2 is now independent of NodeJS and CSInterface.js // // forceEval is now by default true // // It wraps the scripts in a try catch and an eval providing useful error handling // // One can set in the jsx engine $.includeStack = true to return the call stack in the event of an error // /////////////////////////////////////////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////////////////////////////////////// // JSX.js for calling jsx code from the js engine // // 2 methods included // // 1) jsx.evalScript AKA jsx.eval // // 2) jsx.evalFile AKA jsx.file // // Special features // // 1) Allows all changes in your jsx code to be reloaded into your extension at the click of a button // // 2) Can enable the $.fileName property to work and provides a $.__fileName() method as an alternative // // 3) Can force a callBack result from InDesign // // 4) No more csInterface.evalScript('alert("hello "' + title + " " + name + '");') // // use jsx.evalScript('alert("hello __title__ __name__");', {title: title, name: name}); // // 5) execute jsx files from your jsx folder like this jsx.evalFile('myFabJsxScript.jsx'); // // or from a relative path jsx.evalFile('../myFabScripts/myFabJsxScript.jsx'); // // or from an absolute url jsx.evalFile('/Path/to/my/FabJsxScript.jsx'); (mac) // // or from an absolute url jsx.evalFile('C:Path/to/my/FabJsxScript.jsx'); (windows) // // 6) Parameter can be entered in the from of a parameter list which can be in any order or as an object // // 7) Not camelCase sensitive (very useful for the illiterate) // // Dead easy to use BUT SPEND THE 3 TO 5 MINUTES IT SHOULD TAKE TO READ THE INSTRUCTIONS // /////////////////////////////////////////////////////////////////////////////////////////////////////////// /* jshint undef:true, unused:true, esversion:6 */ ////////////////////////////////////// // jsx is the interface for the API // ////////////////////////////////////// var jsx; // Wrap everything in an anonymous function to prevent leeks (function() { ///////////////////////////////////////////////////////////////////// // Substitute some CSInterface functions to avoid dependency on it // ///////////////////////////////////////////////////////////////////// var __dirname = (function() { var path, isMac; path = decodeURI(window.__adobe_cep__.getSystemPath('extension')); isMac = navigator.platform[0] === 'M'; // [M]ac path = path.replace('file://' + (isMac ? '' : '/'), ''); return path; })(); var evalScript = function(script, callback) { callback = callback || function() {}; window.__adobe_cep__.evalScript(script, callback); }; //////////////////////////////////////////// // In place of using the node path module // //////////////////////////////////////////// // jshint undef: true, unused: true // A very minified version of the NodeJs Path module!! // For use outside of NodeJs // Majorly nicked by Trevor from Joyent var path = (function() { var isString = function(arg) { return typeof arg === 'string'; }; // var isObject = function(arg) { // return typeof arg === 'object' && arg !== null; // }; var basename = function(path) { if (!isString(path)) { throw new TypeError('Argument to path.basename must be a string'); } var bits = path.split(/[\/\\]/g); return bits[bits.length - 1]; }; // jshint undef: true // Regex to split a windows path into three parts: [*, device, slash, // tail] windows-only var splitDeviceRe = /^([a-zA-Z]:|[\\\/]{2}[^\\\/]+[\\\/]+[^\\\/]+)?([\\\/])?([\s\S]*?)$/; // Regex to split the tail part of the above into [*, dir, basename, ext] // var splitTailRe = // /^([\s\S]*?)((?:\.{1,2}|[^\\\/]+?|)(\.[^.\/\\]*|))(?:[\\\/]*)$/; var win32 = {}; // Function to split a filename into [root, dir, basename, ext] // var win32SplitPath = function(filename) { // // Separate device+slash from tail // var result = splitDeviceRe.exec(filename), // device = (result[1] || '') + (result[2] || ''), // tail = result[3] || ''; // // Split the tail into dir, basename and extension // var result2 = splitTailRe.exec(tail), // dir = result2[1], // basename = result2[2], // ext = result2[3]; // return [device, dir, basename, ext]; // }; var win32StatPath = function(path) { var result = splitDeviceRe.exec(path), device = result[1] || '', isUnc = !!device && device[1] !== ':'; return { device: device, isUnc: isUnc, isAbsolute: isUnc || !!result[2], // UNC paths are always absolute tail: result[3] }; }; var normalizeUNCRoot = function(device) { return '\\\\' + device.replace(/^[\\\/]+/, '').replace(/[\\\/]+/g, '\\'); }; var normalizeArray = function(parts, allowAboveRoot) { var res = []; for (var i = 0; i < parts.length; i++) { var p = parts[i]; // ignore empty parts if (!p || p === '.') continue; if (p === '..') { if (res.length && res[res.length - 1] !== '..') { res.pop(); } else if (allowAboveRoot) { res.push('..'); } } else { res.push(p); } } return res; }; win32.normalize = function(path) { var result = win32StatPath(path), device = result.device, isUnc = result.isUnc, isAbsolute = result.isAbsolute, tail = result.tail, trailingSlash = /[\\\/]$/.test(tail); // Normalize the tail path tail = normalizeArray(tail.split(/[\\\/]+/), !isAbsolute).join('\\'); if (!tail && !isAbsolute) { tail = '.'; } if (tail && trailingSlash) { tail += '\\'; } // Convert slashes to backslashes when `device` points to an UNC root. // Also squash multiple slashes into a single one where appropriate. if (isUnc) { device = normalizeUNCRoot(device); } return device + (isAbsolute ? '\\' : '') + tail; }; win32.join = function() { var paths = []; for (var i = 0; i < arguments.length; i++) { var arg = arguments[i]; if (!isString(arg)) { throw new TypeError('Arguments to path.join must be strings'); } if (arg) { paths.push(arg); } } var joined = paths.join('\\'); // Make sure that the joined path doesn't start with two slashes, because // normalize() will mistake it for an UNC path then. // // This step is skipped when it is very clear that the user actually // intended to point at an UNC path. This is assumed when the first // non-empty string arguments starts with exactly two slashes followed by // at least one more non-slash character. // // Note that for normalize() to treat a path as an UNC path it needs to // have at least 2 components, so we don't filter for that here. // This means that the user can use join to construct UNC paths from // a server name and a share name; for example: // path.join('//server', 'share') -> '\\\\server\\share\') if (!/^[\\\/]{2}[^\\\/]/.test(paths[0])) { joined = joined.replace(/^[\\\/]{2,}/, '\\'); } return win32.normalize(joined); }; var posix = {}; // posix version posix.join = function() { var path = ''; for (var i = 0; i < arguments.length; i++) { var segment = arguments[i]; if (!isString(segment)) { throw new TypeError('Arguments to path.join must be strings'); } if (segment) { if (!path) { path += segment; } else { path += '/' + segment; } } } return posix.normalize(path); }; // path.normalize(path) // posix version posix.normalize = function(path) { var isAbsolute = path.charAt(0) === '/', trailingSlash = path && path[path.length - 1] === '/'; // Normalize the path path = normalizeArray(path.split('/'), !isAbsolute).join('/'); if (!path && !isAbsolute) { path = '.'; } if (path && trailingSlash) { path += '/'; } return (isAbsolute ? '/' : '') + path; }; win32.basename = posix.basename = basename; this.win32 = win32; this.posix = posix; return (navigator.platform[0] === 'M') ? posix : win32; })(); //////////////////////////////////////////////////////////////////////////////////////////////////////// // The is the "main" function which is to be prototyped // // It run a small snippet in the jsx engine that // // 1) Assigns $.__dirname with the value of the extensions __dirname base path // // 2) Sets up a method $.__fileName() for retrieving from within the jsx script it's $.fileName value // // more on that method later // // At the end of the script the global declaration jsx = new Jsx(); has been made. // // If you like you can remove that and include in your relevant functions // // var jsx = new Jsx(); You would never call the Jsx function without the "new" declaration // //////////////////////////////////////////////////////////////////////////////////////////////////////// var Jsx = function() { var jsxScript; // Setup jsx function to enable the jsx scripts to easily retrieve their file location jsxScript = [ '$.level = 0;', 'if(!$.__fileNames){', ' $.__fileNames = {};', ' $.__dirname = "__dirname__";'.replace('__dirname__', __dirname), ' $.__fileName = function(name){', ' name = name || $.fileName;', ' return ($.__fileNames && $.__fileNames[name]) || $.fileName;', ' };', '}' ].join(''); evalScript(jsxScript); return this; }; /** * [evalScript] For calling jsx scripts from the js engine * * The jsx.evalScript method is used for calling jsx scripts directly from the js engine * Allows for easy replacement i.e. variable insertions and for forcing eval. * For convenience jsx.eval or jsx.script or jsx.evalscript can be used instead of calling jsx.evalScript * * @param {String} jsxScript * The string that makes up the jsx script * it can contain a simple template like syntax for replacements * 'alert("__foo__");' * the __foo__ will be replaced as per the replacements parameter * * @param {Function} callback * The callback function you want the jsx script to trigger on completion * The result of the jsx script is passed as the argument to that function * The function can exist in some other file. * Note that InDesign does not automatically pass the callBack as a string. * Either write your InDesign in a way that it returns a sting the form of * return 'this is my result surrounded by quotes' * or use the force eval option * [Optional DEFAULT no callBack] * * @param {Object} replacements * The replacements to make on the jsx script * given the following script (template) * 'alert("__message__: " + __val__);' * and we want to change the script to * 'alert("I was born in the year: " + 1234);' * we would pass the following object * {"message": 'I was born in the year', "val": 1234} * or if not using reserved words like do we can leave out the key quotes * {message: 'I was born in the year', val: 1234} * [Optional DEFAULT no replacements] * * @param {Bolean} forceEval * If the script should be wrapped in an eval and try catch * This will 1) provide useful error feedback if heaven forbid it is needed * 2) The result will be a string which is required for callback results in InDesign * [Optional DEFAULT true] * * Note 1) The order of the parameters is irrelevant * Note 2) One can pass the arguments as an object if desired * jsx.evalScript(myCallBackFunction, 'alert("__myMessage__");', true); * is the same as * jsx.evalScript({ * script: 'alert("__myMessage__");', * replacements: {myMessage: 'Hi there'}, * callBack: myCallBackFunction, * eval: true * }); * note that either lower or camelCase key names are valid * i.e. both callback or callBack will work * * The following keys are the same jsx || script || jsxScript || jsxscript || file * The following keys are the same callBack || callback * The following keys are the same replacements || replace * The following keys are the same eval || forceEval || forceeval * The following keys are the same forceEvalScript || forceevalscript || evalScript || evalscript; * * @return {Boolean} if the jsxScript was executed or not */ Jsx.prototype.evalScript = function() { var arg, i, key, replaceThis, withThis, args, callback, forceEval, replacements, jsxScript, isBin; ////////////////////////////////////////////////////////////////////////////////////// // sort out order which arguments into jsxScript, callback, replacements, forceEval // ////////////////////////////////////////////////////////////////////////////////////// args = arguments; // Detect if the parameters were passed as an object and if so allow for various keys if (args.length === 1 && (arg = args[0]) instanceof Object) { jsxScript = arg.jsxScript || arg.jsx || arg.script || arg.file || arg.jsxscript; callback = arg.callBack || arg.callback; replacements = arg.replacements || arg.replace; forceEval = arg.eval || arg.forceEval || arg.forceeval; } else { for (i = 0; i < 4; i++) { arg = args[i]; if (arg === undefined) { continue; } if (arg.constructor === String) { jsxScript = arg; continue; } if (arg.constructor === Object) { replacements = arg; continue; } if (arg.constructor === Function) { callback = arg; continue; } if (arg === false) { forceEval = false; } } } // If no script provide then not too much to do! if (!jsxScript) { return false; } // Have changed the forceEval default to be true as I prefer the error handling if (forceEval !== false) { forceEval = true; } ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // On Illustrator and other apps the result of the jsx script is automatically passed as a string // // if you have a "script" containing the single number 1 and nothing else then the callBack will register as "1" // // On InDesign that same script will provide a blank callBack // // Let's say we have a callBack function var callBack = function(result){alert(result);} // // On Ai your see the 1 in the alert // // On ID your just see a blank alert // // To see the 1 in the alert you need to convert the result to a string and then it will show // // So if we rewrite out 1 byte script to '1' i.e. surround the 1 in quotes then the call back alert will show 1 // // If the scripts planed one can make sure that the results always passed as a string (including errors) // // otherwise one can wrap the script in an eval and then have the result passed as a string // // I have not gone through all the apps but can say // // for Ai you never need to set the forceEval to true // // for ID you if you have not coded your script appropriately and your want to send a result to the callBack then set forceEval to true // // I changed this that even on Illustrator it applies the try catch, Note the try catch will fail if $.level is set to 1 // ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// if (forceEval) { isBin = (jsxScript.substring(0, 10) === '@JSXBIN@ES') ? '' : '\n'; jsxScript = ( // "\n''') + '';} catch(e){(function(e){var n, a=[]; for (n in e){a.push(n + ': ' + e[n])}; return a.join('\n')})(e)}"); // "\n''') + '';} catch(e){e + (e.line ? ('\\nLine ' + (+e.line - 1)) : '')}"); [ "$.level = 0;", "try{eval('''" + isBin, // need to add an extra line otherwise #targetengine doesn't work ;-] jsxScript.replace(/\\/g, '\\\\').replace(/'/g, "\\'").replace(/"/g, '\\"') + "\n''') + '';", "} catch (e) {", " (function(e) {", " var line, sourceLine, name, description, ErrorMessage, fileName, start, end, bug;", " line = +e.line" + (isBin === '' ? ';' : ' - 1;'), // To take into account the extra line added " fileName = File(e.fileName).fsName;", " sourceLine = line && e.source.split(/[\\r\\n]/)[line];", " name = e.name;", " description = e.description;", " ErrorMessage = name + ' ' + e.number + ': ' + description;", " if (fileName.length && !(/[\\/\\\\]\\d+$/.test(fileName))) {", " ErrorMessage += '\\nFile: ' + fileName;", " line++;", " }", " if (line){", " ErrorMessage += '\\nLine: ' + line +", " '-> ' + ((sourceLine.length < 300) ? sourceLine : sourceLine.substring(0,300) + '...');", " }", " if (e.start) {ErrorMessage += '\\nBug: ' + e.source.substring(e.start - 1, e.end)}", " if ($.includeStack) {ErrorMessage += '\\nStack:' + $.stack;}", " return ErrorMessage;", " })(e);", "}" ].join('') ); } ///////////////////////////////////////////////////////////// // deal with the replacements // // Note it's probably better to use ${template} `literals` // ///////////////////////////////////////////////////////////// if (replacements) { for (key in replacements) { if (replacements.hasOwnProperty(key)) { replaceThis = new RegExp('__' + key + '__', 'g'); withThis = replacements[key]; jsxScript = jsxScript.replace(replaceThis, withThis + ''); } } } try { evalScript(jsxScript, callback); return true; } catch (err) { //////////////////////////////////////////////// // Do whatever error handling you want here ! // //////////////////////////////////////////////// var newErr; newErr = new Error(err); alert('Error Eek: ' + newErr.stack); return false; } }; /** * [evalFile] For calling jsx scripts from the js engine * * The jsx.evalFiles method is used for executing saved jsx scripts * where the jsxScript parameter is a string of the jsx scripts file location. * For convenience jsx.file or jsx.evalfile can be used instead of jsx.evalFile * * @param {String} file * The path to jsx script * If only the base name is provided then the path will be presumed to be the * To execute files stored in the jsx folder located in the __dirname folder use * jsx.evalFile('myFabJsxScript.jsx'); * To execute files stored in the a folder myFabScripts located in the __dirname folder use * jsx.evalFile('./myFabScripts/myFabJsxScript.jsx'); * To execute files stored in the a folder myFabScripts located at an absolute url use * jsx.evalFile('/Path/to/my/FabJsxScript.jsx'); (mac) * or jsx.evalFile('C:Path/to/my/FabJsxScript.jsx'); (windows) * * @param {Function} callback * The callback function you want the jsx script to trigger on completion * The result of the jsx script is passed as the argument to that function * The function can exist in some other file. * Note that InDesign does not automatically pass the callBack as a string. * Either write your InDesign in a way that it returns a sting the form of * return 'this is my result surrounded by quotes' * or use the force eval option * [Optional DEFAULT no callBack] * * @param {Object} replacements * The replacements to make on the jsx script * give the following script (template) * 'alert("__message__: " + __val__);' * and we want to change the script to * 'alert("I was born in the year: " + 1234);' * we would pass the following object * {"message": 'I was born in the year', "val": 1234} * or if not using reserved words like do we can leave out the key quotes * {message: 'I was born in the year', val: 1234} * By default when possible the forceEvalScript will be set to true * The forceEvalScript option cannot be true when there are replacements * To force the forceEvalScript to be false you can send a blank set of replacements * jsx.evalFile('myFabScript.jsx', {}); Will NOT be executed using the $.evalScript method * jsx.evalFile('myFabScript.jsx'); Will YES be executed using the $.evalScript method * see the forceEvalScript parameter for details on this * [Optional DEFAULT no replacements] * * @param {Bolean} forceEval * If the script should be wrapped in an eval and try catch * This will 1) provide useful error feedback if heaven forbid it is needed * 2) The result will be a string which is required for callback results in InDesign * [Optional DEFAULT true] * * If no replacements are needed then the jsx script is be executed by using the $.evalFile method * This exposes the true value of the $.fileName property * In such a case it's best to avoid using the $.__fileName() with no base name as it won't work * BUT one can still use the $.__fileName('baseName') method which is more accurate than the standard $.fileName property * Let's say you have a Drive called "Graphics" AND YOU HAVE a root folder on your "main" drive called "Graphics" * You call a script jsx.evalFile('/Volumes/Graphics/myFabScript.jsx'); * $.fileName will give you '/Graphics/myFabScript.jsx' which is wrong * $.__fileName('myFabScript.jsx') will give you '/Volumes/Graphics/myFabScript.jsx' which is correct * $.__fileName() will not give you a reliable result * Note that if your calling multiple versions of myFabScript.jsx stored in multiple folders then you can get stuffed! * i.e. if the fileName is important to you then don't do that. * It also will force the result of the jsx file as a string which is particularly useful for InDesign callBacks * * Note 1) The order of the parameters is irrelevant * Note 2) One can pass the arguments as an object if desired * jsx.evalScript(myCallBackFunction, 'alert("__myMessage__");', true); * is the same as * jsx.evalScript({ * script: 'alert("__myMessage__");', * replacements: {myMessage: 'Hi there'}, * callBack: myCallBackFunction, * eval: false, * }); * note that either lower or camelCase key names or valid * i.e. both callback or callBack will work * * The following keys are the same file || jsx || script || jsxScript || jsxscript * The following keys are the same callBack || callback * The following keys are the same replacements || replace * The following keys are the same eval || forceEval || forceeval * * @return {Boolean} if the jsxScript was executed or not */ Jsx.prototype.evalFile = function() { var arg, args, callback, fileName, fileNameScript, forceEval, forceEvalScript, i, jsxFolder, jsxScript, newLine, replacements, success; success = true; // optimistic args = arguments; jsxFolder = path.join(__dirname, 'jsx'); ////////////////////////////////////////////////////////////////////////////////////////////////////////// // $.fileName does not return it's correct path in the jsx engine for files called from the js engine // // In Illustrator it returns an integer in InDesign it returns an empty string // // This script injection allows for the script to know it's path by calling // // $.__fileName(); // // on Illustrator this works pretty well // // on InDesign it's best to use with a bit of care // // If the a second script has been called the InDesing will "forget" the path to the first script // // 2 work-arounds for this // // 1) at the beginning of your script add var thePathToMeIs = $.fileName(); // // thePathToMeIs will not be forgotten after running the second script // // 2) $.__fileName('myBaseName.jsx'); // // for example you have file with the following path // // /path/to/me.jsx // // Call $.__fileName('me.jsx') and you will get /path/to/me.jsx even after executing a second script // // Note When the forceEvalScript option is used then you just use the regular $.fileName property // ////////////////////////////////////////////////////////////////////////////////////////////////////////// fileNameScript = [ // The if statement should not normally be executed 'if(!$.__fileNames){', ' $.__fileNames = {};', ' $.__dirname = "__dirname__";'.replace('__dirname__', __dirname), ' $.__fileName = function(name){', ' name = name || $.fileName;', ' return ($.__fileNames && $.__fileNames[name]) || $.fileName;', ' };', '}', '$.__fileNames["__basename__"] = $.__fileNames["" + $.fileName] = "__fileName__";' ].join(''); ////////////////////////////////////////////////////////////////////////////////////// // sort out order which arguments into jsxScript, callback, replacements, forceEval // ////////////////////////////////////////////////////////////////////////////////////// // Detect if the parameters were passed as an object and if so allow for various keys if (args.length === 1 && (arg = args[0]) instanceof Object) { jsxScript = arg.jsxScript || arg.jsx || arg.script || arg.file || arg.jsxscript; callback = arg.callBack || arg.callback; replacements = arg.replacements || arg.replace; forceEval = arg.eval || arg.forceEval || arg.forceeval; } else { for (i = 0; i < 5; i++) { arg = args[i]; if (arg === undefined) { continue; } if (arg.constructor.name === 'String') { jsxScript = arg; continue; } if (arg.constructor.name === 'Object') { ////////////////////////////////////////////////////////////////////////////////////////////////////////////// // If no replacements are provided then the $.evalScript method will be used // // This will allow directly for the $.fileName property to be used // // If one does not want the $.evalScript method to be used then // // either send a blank object as the replacements {} // // or explicitly set the forceEvalScript option to false // // This can only be done if the parameters are passed as an object // // i.e. jsx.evalFile({file:'myFabScript.jsx', forceEvalScript: false}); // // if the file was called using // // i.e. jsx.evalFile('myFabScript.jsx'); // // then the following jsx code is called $.evalFile(new File('Path/to/myFabScript.jsx', 10000000000)) + ''; // // forceEval is never needed if the forceEvalScript is triggered // ////////////////////////////////////////////////////////////////////////////////////////////////////////////// replacements = arg; continue; } if (arg.constructor === Function) { callback = arg; continue; } if (arg === false) { forceEval = false; } } } // If no script provide then not too much to do! if (!jsxScript) { return false; } forceEvalScript = !replacements; ////////////////////////////////////////////////////// // Get path of script // // Check if it's literal, relative or in jsx folder // ////////////////////////////////////////////////////// if (/^\/|[a-zA-Z]+:/.test(jsxScript)) { // absolute path Mac | Windows jsxScript = path.normalize(jsxScript); } else if (/^\.+\//.test(jsxScript)) { jsxScript = path.join(__dirname, jsxScript); // relative path } else { jsxScript = path.join(jsxFolder, jsxScript); // files in the jsxFolder } if (forceEvalScript) { jsxScript = jsxScript.replace(/"/g, '\\"'); // Check that the path exist, should change this to asynchronous at some point if (!window.cep.fs.stat(jsxScript).err) { jsxScript = fileNameScript.replace(/__fileName__/, jsxScript).replace(/__basename__/, path.basename(jsxScript)) + '$.evalFile(new File("' + jsxScript.replace(/\\/g, '\\\\') + '")) + "";'; return this.evalScript(jsxScript, callback, forceEval); } else { throw new Error(`The file: {jsxScript} could not be found / read`); } } //////////////////////////////////////////////////////////////////////////////////////////////// // Replacements made so we can't use $.evalFile and need to read the jsx script for ourselves // //////////////////////////////////////////////////////////////////////////////////////////////// fileName = jsxScript.replace(/\\/g, '\\\\').replace(/"/g, '\\"'); try { jsxScript = window.cep.fs.readFile(jsxScript).data; } catch (er) { throw new Error(`The file: ${fileName} could not be read`); } // It is desirable that the injected fileNameScript is on the same line as the 1st line of the script // This is so that the $.line or error.line returns the same value as the actual file // However if the 1st line contains a # directive then we need to insert a new line and stuff the above problem // When possible i.e. when there's no replacements then $.evalFile will be used and then the whole issue is avoided newLine = /^\s*#/.test(jsxScript) ? '\n' : ''; jsxScript = fileNameScript.replace(/__fileName__/, fileName).replace(/__basename__/, path.basename(fileName)) + newLine + jsxScript; try { // evalScript(jsxScript, callback); return this.evalScript(jsxScript, callback, replacements, forceEval); } catch (err) { //////////////////////////////////////////////// // Do whatever error handling you want here ! // //////////////////////////////////////////////// var newErr; newErr = new Error(err); alert('Error Eek: ' + newErr.stack); return false; } return success; // success should be an array but for now it's a Boolean }; //////////////////////////////////// // Setup alternative method names // //////////////////////////////////// Jsx.prototype.eval = Jsx.prototype.script = Jsx.prototype.evalscript = Jsx.prototype.evalScript; Jsx.prototype.file = Jsx.prototype.evalfile = Jsx.prototype.evalFile; /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // Examples // // jsx.evalScript('alert("foo");'); // // jsx.evalFile('foo.jsx'); // where foo.jsx is stored in the jsx folder at the base of the extensions directory // // jsx.evalFile('../myFolder/foo.jsx'); // where a relative or absolute file path is given // // // // using conventional methods one would use in the case were the values to swap were supplied by variables // // csInterface.evalScript('var q = "' + name + '"; alert("' + myString + '" ' + myOp + ' q);q;', callback); // // Using all the '' + foo + '' is very error prone // // jsx.evalScript('var q = "__name__"; alert(__string__ __opp__ q);q;',{'name':'Fred', 'string':'Hello ', 'opp':'+'}, callBack); // // is much simpler and less error prone // // // // more readable to use object // // jsx.evalFile({ // // file: 'yetAnotherFabScript.jsx', // // replacements: {"this": foo, That: bar, and: "&&", the: foo2, other: bar2}, // // eval: true // // }) // // Enjoy // /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// jsx = new Jsx(); })(); ================================================ FILE: openpype/hosts/photoshop/api/extension/host/index.jsx ================================================ #include "json.js"; #target photoshop var LogFactory=function(file,write,store,level,defaultStatus,continuing){if(file&&(file.constructor===String||file.constructor===File)){file={file:file};}else if(!file)file={file:{}};write=(file.write!==undefined)?file.write:write;if(write===undefined){write=true;}store=(file.store!==undefined)?file.store||false:store||false;level=(file.level!==undefined)?file.level:level;defaultStatus=(file.defaultStatus!==undefined)?file.defaultStatus:defaultStatus;if(defaultStatus===undefined){defaultStatus='LOG';}continuing=(file.continuing!==undefined)?file.continuing:continuing||false;file=file.file||{};var stack,times,logTime,logPoint,icons,statuses,LOG_LEVEL,LOG_STATUS;stack=[];times=[];logTime=new Date();logPoint='Log Factory Start';icons={"1":"\ud83d\udd50","130":"\ud83d\udd5c","2":"\ud83d\udd51","230":"\ud83d\udd5d","3":"\ud83d\udd52","330":"\ud83d\udd5e","4":"\ud83d\udd53","430":"\ud83d\udd5f","5":"\ud83d\udd54","530":"\ud83d\udd60","6":"\ud83d\udd55","630":"\ud83d\udd61","7":"\ud83d\udd56","730":"\ud83d\udd62","8":"\ud83d\udd57","830":"\ud83d\udd63","9":"\ud83d\udd58","930":"\ud83d\udd64","10":"\ud83d\udd59","1030":"\ud83d\udd65","11":"\ud83d\udd5a","1130":"\ud83d\udd66","12":"\ud83d\udd5b","1230":"\ud83d\udd67","AIRPLANE":"\ud83d\udee9","ALARM":"\u23f0","AMBULANCE":"\ud83d\ude91","ANCHOR":"\u2693","ANGRY":"\ud83d\ude20","ANGUISHED":"\ud83d\ude27","ANT":"\ud83d\udc1c","ANTENNA":"\ud83d\udce1","APPLE":"\ud83c\udf4f","APPLE2":"\ud83c\udf4e","ATM":"\ud83c\udfe7","ATOM":"\u269b","BABYBOTTLE":"\ud83c\udf7c","BAD:":"\ud83d\udc4e","BANANA":"\ud83c\udf4c","BANDAGE":"\ud83e\udd15","BANK":"\ud83c\udfe6","BATTERY":"\ud83d\udd0b","BED":"\ud83d\udecf","BEE":"\ud83d\udc1d","BEER":"\ud83c\udf7a","BELL":"\ud83d\udd14","BELLOFF":"\ud83d\udd15","BIRD":"\ud83d\udc26","BLACKFLAG":"\ud83c\udff4","BLUSH":"\ud83d\ude0a","BOMB":"\ud83d\udca3","BOOK":"\ud83d\udcd5","BOOKMARK":"\ud83d\udd16","BOOKS":"\ud83d\udcda","BOW":"\ud83c\udff9","BOWLING":"\ud83c\udfb3","BRIEFCASE":"\ud83d\udcbc","BROKEN":"\ud83d\udc94","BUG":"\ud83d\udc1b","BUILDING":"\ud83c\udfdb","BUILDINGS":"\ud83c\udfd8","BULB":"\ud83d\udca1","BUS":"\ud83d\ude8c","CACTUS":"\ud83c\udf35","CALENDAR":"\ud83d\udcc5","CAMEL":"\ud83d\udc2a","CAMERA":"\ud83d\udcf7","CANDLE":"\ud83d\udd6f","CAR":"\ud83d\ude98","CAROUSEL":"\ud83c\udfa0","CASTLE":"\ud83c\udff0","CATEYES":"\ud83d\ude3b","CATJOY":"\ud83d\ude39","CATMOUTH":"\ud83d\ude3a","CATSMILE":"\ud83d\ude3c","CD":"\ud83d\udcbf","CHECK":"\u2714","CHEQFLAG":"\ud83c\udfc1","CHICK":"\ud83d\udc25","CHICKEN":"\ud83d\udc14","CHICKHEAD":"\ud83d\udc24","CIRCLEBLACK":"\u26ab","CIRCLEBLUE":"\ud83d\udd35","CIRCLERED":"\ud83d\udd34","CIRCLEWHITE":"\u26aa","CIRCUS":"\ud83c\udfaa","CLAPPER":"\ud83c\udfac","CLAPPING":"\ud83d\udc4f","CLIP":"\ud83d\udcce","CLIPBOARD":"\ud83d\udccb","CLOUD":"\ud83c\udf28","CLOVER":"\ud83c\udf40","CLOWN":"\ud83e\udd21","COLDSWEAT":"\ud83d\ude13","COLDSWEAT2":"\ud83d\ude30","COMPRESS":"\ud83d\udddc","CONFOUNDED":"\ud83d\ude16","CONFUSED":"\ud83d\ude15","CONSTRUCTION":"\ud83d\udea7","CONTROL":"\ud83c\udf9b","COOKIE":"\ud83c\udf6a","COOKING":"\ud83c\udf73","COOL":"\ud83d\ude0e","COOLBOX":"\ud83c\udd92","COPYRIGHT":"\u00a9","CRANE":"\ud83c\udfd7","CRAYON":"\ud83d\udd8d","CREDITCARD":"\ud83d\udcb3","CROSS":"\u2716","CROSSBOX:":"\u274e","CRY":"\ud83d\ude22","CRYCAT":"\ud83d\ude3f","CRYSTALBALL":"\ud83d\udd2e","CUSTOMS":"\ud83d\udec3","DELICIOUS":"\ud83d\ude0b","DERELICT":"\ud83c\udfda","DESKTOP":"\ud83d\udda5","DIAMONDLB":"\ud83d\udd37","DIAMONDLO":"\ud83d\udd36","DIAMONDSB":"\ud83d\udd39","DIAMONDSO":"\ud83d\udd38","DICE":"\ud83c\udfb2","DISAPPOINTED":"\ud83d\ude1e","CRY2":"\ud83d\ude25","DIVISION":"\u2797","DIZZY":"\ud83d\ude35","DOLLAR":"\ud83d\udcb5","DOLLAR2":"\ud83d\udcb2","DOWNARROW":"\u2b07","DVD":"\ud83d\udcc0","EJECT":"\u23cf","ELEPHANT":"\ud83d\udc18","EMAIL":"\ud83d\udce7","ENVELOPE":"\ud83d\udce8","ENVELOPE2":"\u2709","ENVELOPE_DOWN":"\ud83d\udce9","EURO":"\ud83d\udcb6","EVIL":"\ud83d\ude08","EXPRESSIONLESS":"\ud83d\ude11","EYES":"\ud83d\udc40","FACTORY":"\ud83c\udfed","FAX":"\ud83d\udce0","FEARFUL":"\ud83d\ude28","FILEBOX":"\ud83d\uddc3","FILECABINET":"\ud83d\uddc4","FIRE":"\ud83d\udd25","FIREENGINE":"\ud83d\ude92","FIST":"\ud83d\udc4a","FLOWER":"\ud83c\udf37","FLOWER2":"\ud83c\udf38","FLUSHED":"\ud83d\ude33","FOLDER":"\ud83d\udcc1","FOLDER2":"\ud83d\udcc2","FREE":"\ud83c\udd93","FROG":"\ud83d\udc38","FROWN":"\ud83d\ude41","GEAR":"\u2699","GLOBE":"\ud83c\udf0d","GLOWINGSTAR":"\ud83c\udf1f","GOOD:":"\ud83d\udc4d","GRIMACING":"\ud83d\ude2c","GRIN":"\ud83d\ude00","GRINNINGCAT":"\ud83d\ude38","HALO":"\ud83d\ude07","HAMMER":"\ud83d\udd28","HAMSTER":"\ud83d\udc39","HAND":"\u270b","HANDDOWN":"\ud83d\udc47","HANDLEFT":"\ud83d\udc48","HANDRIGHT":"\ud83d\udc49","HANDUP":"\ud83d\udc46","HATCHING":"\ud83d\udc23","HAZARD":"\u2623","HEADPHONE":"\ud83c\udfa7","HEARNOEVIL":"\ud83d\ude49","HEARTBLUE":"\ud83d\udc99","HEARTEYES":"\ud83d\ude0d","HEARTGREEN":"\ud83d\udc9a","HEARTYELLOW":"\ud83d\udc9b","HELICOPTER":"\ud83d\ude81","HERB":"\ud83c\udf3f","HIGH_BRIGHTNESS":"\ud83d\udd06","HIGHVOLTAGE":"\u26a1","HIT":"\ud83c\udfaf","HONEY":"\ud83c\udf6f","HOT":"\ud83c\udf36","HOURGLASS":"\u23f3","HOUSE":"\ud83c\udfe0","HUGGINGFACE":"\ud83e\udd17","HUNDRED":"\ud83d\udcaf","HUSHED":"\ud83d\ude2f","ID":"\ud83c\udd94","INBOX":"\ud83d\udce5","INDEX":"\ud83d\uddc2","JOY":"\ud83d\ude02","KEY":"\ud83d\udd11","KISS":"\ud83d\ude18","KISS2":"\ud83d\ude17","KISS3":"\ud83d\ude19","KISS4":"\ud83d\ude1a","KISSINGCAT":"\ud83d\ude3d","KNIFE":"\ud83d\udd2a","LABEL":"\ud83c\udff7","LADYBIRD":"\ud83d\udc1e","LANDING":"\ud83d\udeec","LAPTOP":"\ud83d\udcbb","LEFTARROW":"\u2b05","LEMON":"\ud83c\udf4b","LIGHTNINGCLOUD":"\ud83c\udf29","LINK":"\ud83d\udd17","LITTER":"\ud83d\udeae","LOCK":"\ud83d\udd12","LOLLIPOP":"\ud83c\udf6d","LOUDSPEAKER":"\ud83d\udce2","LOW_BRIGHTNESS":"\ud83d\udd05","MAD":"\ud83d\ude1c","MAGNIFYING_GLASS":"\ud83d\udd0d","MASK":"\ud83d\ude37","MEDAL":"\ud83c\udf96","MEMO":"\ud83d\udcdd","MIC":"\ud83c\udfa4","MICROSCOPE":"\ud83d\udd2c","MINUS":"\u2796","MOBILE":"\ud83d\udcf1","MONEY":"\ud83d\udcb0","MONEYMOUTH":"\ud83e\udd11","MONKEY":"\ud83d\udc35","MOUSE":"\ud83d\udc2d","MOUSE2":"\ud83d\udc01","MOUTHLESS":"\ud83d\ude36","MOVIE":"\ud83c\udfa5","MUGS":"\ud83c\udf7b","NERD":"\ud83e\udd13","NEUTRAL":"\ud83d\ude10","NEW":"\ud83c\udd95","NOENTRY":"\ud83d\udeab","NOTEBOOK":"\ud83d\udcd4","NOTEPAD":"\ud83d\uddd2","NUTANDBOLT":"\ud83d\udd29","O":"\u2b55","OFFICE":"\ud83c\udfe2","OK":"\ud83c\udd97","OKHAND":"\ud83d\udc4c","OLDKEY":"\ud83d\udddd","OPENLOCK":"\ud83d\udd13","OPENMOUTH":"\ud83d\ude2e","OUTBOX":"\ud83d\udce4","PACKAGE":"\ud83d\udce6","PAGE":"\ud83d\udcc4","PAINTBRUSH":"\ud83d\udd8c","PALETTE":"\ud83c\udfa8","PANDA":"\ud83d\udc3c","PASSPORT":"\ud83d\udec2","PAWS":"\ud83d\udc3e","PEN":"\ud83d\udd8a","PEN2":"\ud83d\udd8b","PENSIVE":"\ud83d\ude14","PERFORMING":"\ud83c\udfad","PHONE":"\ud83d\udcde","PILL":"\ud83d\udc8a","PING":"\u2757","PLATE":"\ud83c\udf7d","PLUG":"\ud83d\udd0c","PLUS":"\u2795","POLICE":"\ud83d\ude93","POLICELIGHT":"\ud83d\udea8","POSTOFFICE":"\ud83c\udfe4","POUND":"\ud83d\udcb7","POUTING":"\ud83d\ude21","POUTINGCAT":"\ud83d\ude3e","PRESENT":"\ud83c\udf81","PRINTER":"\ud83d\udda8","PROJECTOR":"\ud83d\udcfd","PUSHPIN":"\ud83d\udccc","QUESTION":"\u2753","RABBIT":"\ud83d\udc30","RADIOACTIVE":"\u2622","RADIOBUTTON":"\ud83d\udd18","RAINCLOUD":"\ud83c\udf27","RAT":"\ud83d\udc00","RECYCLE":"\u267b","REGISTERED":"\u00ae","RELIEVED":"\ud83d\ude0c","ROBOT":"\ud83e\udd16","ROCKET":"\ud83d\ude80","ROLLING":"\ud83d\ude44","ROOSTER":"\ud83d\udc13","RULER":"\ud83d\udccf","SATELLITE":"\ud83d\udef0","SAVE":"\ud83d\udcbe","SCHOOL":"\ud83c\udfeb","SCISSORS":"\u2702","SCREAMING":"\ud83d\ude31","SCROLL":"\ud83d\udcdc","SEAT":"\ud83d\udcba","SEEDLING":"\ud83c\udf31","SEENOEVIL":"\ud83d\ude48","SHIELD":"\ud83d\udee1","SHIP":"\ud83d\udea2","SHOCKED":"\ud83d\ude32","SHOWER":"\ud83d\udebf","SLEEPING":"\ud83d\ude34","SLEEPY":"\ud83d\ude2a","SLIDER":"\ud83c\udf9a","SLOT":"\ud83c\udfb0","SMILE":"\ud83d\ude42","SMILING":"\ud83d\ude03","SMILINGCLOSEDEYES":"\ud83d\ude06","SMILINGEYES":"\ud83d\ude04","SMILINGSWEAT":"\ud83d\ude05","SMIRK":"\ud83d\ude0f","SNAIL":"\ud83d\udc0c","SNAKE":"\ud83d\udc0d","SOCCER":"\u26bd","SOS":"\ud83c\udd98","SPEAKER":"\ud83d\udd08","SPEAKEROFF":"\ud83d\udd07","SPEAKNOEVIL":"\ud83d\ude4a","SPIDER":"\ud83d\udd77","SPIDERWEB":"\ud83d\udd78","STAR":"\u2b50","STOP":"\u26d4","STOPWATCH":"\u23f1","SULK":"\ud83d\ude26","SUNFLOWER":"\ud83c\udf3b","SUNGLASSES":"\ud83d\udd76","SYRINGE":"\ud83d\udc89","TAKEOFF":"\ud83d\udeeb","TAXI":"\ud83d\ude95","TELESCOPE":"\ud83d\udd2d","TEMPORATURE":"\ud83e\udd12","TENNIS":"\ud83c\udfbe","THERMOMETER":"\ud83c\udf21","THINKING":"\ud83e\udd14","THUNDERCLOUD":"\u26c8","TICKBOX":"\u2705","TICKET":"\ud83c\udf9f","TIRED":"\ud83d\ude2b","TOILET":"\ud83d\udebd","TOMATO":"\ud83c\udf45","TONGUE":"\ud83d\ude1b","TOOLS":"\ud83d\udee0","TORCH":"\ud83d\udd26","TORNADO":"\ud83c\udf2a","TOUNG2":"\ud83d\ude1d","TRADEMARK":"\u2122","TRAFFICLIGHT":"\ud83d\udea6","TRASH":"\ud83d\uddd1","TREE":"\ud83c\udf32","TRIANGLE_LEFT":"\u25c0","TRIANGLE_RIGHT":"\u25b6","TRIANGLEDOWN":"\ud83d\udd3b","TRIANGLEUP":"\ud83d\udd3a","TRIANGULARFLAG":"\ud83d\udea9","TROPHY":"\ud83c\udfc6","TRUCK":"\ud83d\ude9a","TRUMPET":"\ud83c\udfba","TURKEY":"\ud83e\udd83","TURTLE":"\ud83d\udc22","UMBRELLA":"\u26f1","UNAMUSED":"\ud83d\ude12","UPARROW":"\u2b06","UPSIDEDOWN":"\ud83d\ude43","WARNING":"\u26a0","WATCH":"\u231a","WAVING":"\ud83d\udc4b","WEARY":"\ud83d\ude29","WEARYCAT":"\ud83d\ude40","WHITEFLAG":"\ud83c\udff3","WINEGLASS":"\ud83c\udf77","WINK":"\ud83d\ude09","WORRIED":"\ud83d\ude1f","WRENCH":"\ud83d\udd27","X":"\u274c","YEN":"\ud83d\udcb4","ZIPPERFACE":"\ud83e\udd10","UNDEFINED":"","":""};statuses={F:'FATAL',B:'BUG',C:'CRITICAL',E:'ERROR',W:'WARNING',I:'INFO',IM:'IMPORTANT',D:'DEBUG',L:'LOG',CO:'CONSTANT',FU:'FUNCTION',R:'RETURN',V:'VARIABLE',S:'STACK',RE:'RESULT',ST:'STOPPER',TI:'TIMER',T:'TRACE'};LOG_LEVEL={NONE:7,OFF:7,FATAL:6,ERROR:5,WARN:4,INFO:3,UNDEFINED:2,'':2,DEFAULT:2,DEBUG:2,TRACE:1,ON:0,ALL:0,};LOG_STATUS={OFF:LOG_LEVEL.OFF,NONE:LOG_LEVEL.OFF,NO:LOG_LEVEL.OFF,NOPE:LOG_LEVEL.OFF,FALSE:LOG_LEVEL.OFF,FATAL:LOG_LEVEL.FATAL,BUG:LOG_LEVEL.ERROR,CRITICAL:LOG_LEVEL.ERROR,ERROR:LOG_LEVEL.ERROR,WARNING:LOG_LEVEL.WARN,INFO:LOG_LEVEL.INFO,IMPORTANT:LOG_LEVEL.INFO,DEBUG:LOG_LEVEL.DEBUG,LOG:LOG_LEVEL.DEBUG,STACK:LOG_LEVEL.DEBUG,CONSTANT:LOG_LEVEL.DEBUG,FUNCTION:LOG_LEVEL.DEBUG,VARIABLE:LOG_LEVEL.DEBUG,RETURN:LOG_LEVEL.DEBUG,RESULT:LOG_LEVEL.TRACE,STOPPER:LOG_LEVEL.TRACE,TIMER:LOG_LEVEL.TRACE,TRACE:LOG_LEVEL.TRACE,ALL:LOG_LEVEL.ALL,YES:LOG_LEVEL.ALL,YEP:LOG_LEVEL.ALL,TRUE:LOG_LEVEL.ALL};var logFile,logFolder;var LOG=function(message,status,icon){if(LOG.level!==LOG_LEVEL.OFF&&(LOG.write||LOG.store)&&LOG.arguments.length)return LOG.addMessage(message,status,icon);};LOG.logDecodeLevel=function(level){if(level==~~level)return Math.abs(level);var lev;level+='';level=level.toUpperCase();if(level in statuses){level=statuses[level];}lev=LOG_LEVEL[level];if(lev!==undefined)return lev;lev=LOG_STATUS[level];if(lev!==undefined)return lev;return LOG_LEVEL.DEFAULT;};LOG.write=write;LOG.store=store;LOG.level=LOG.logDecodeLevel(level);LOG.status=defaultStatus;LOG.addMessage=function(message,status,icon){var date=new Date(),count,bool,logStatus;if(status&&status.constructor.name==='String'){status=status.toUpperCase();status=statuses[status]||status;}else status=LOG.status;logStatus=LOG_STATUS[status]||LOG_STATUS.ALL;if(logStatus- -1.0 when error occurs
\n *- 1.0 means normal screen
\n *- >1.0 means HiDPI screen
\n *999)?'['+LOG.count+'] ':(' ['+LOG.count+'] ').slice(-7);message=count+status+icon+(message instanceof Object?message.toSource():message)+date;if(LOG.store){stack.push(message);}if(LOG.write){bool=file&&file.writable&&logFile.writeln(message);if(!bool){file.writable=true;LOG.setFile(logFile);logFile.writeln(message);}}LOG.count++;return true;};var logNewFile=function(file,isCookie,overwrite){file.encoding='UTF-8';file.lineFeed=($.os[0]=='M')?'Macintosh':' Windows';if(isCookie)return file.open(overwrite?'w':'e')&&file;file.writable=LOG.write;logFile=file;logFolder=file.parent;if(continuing){LOG.count=LOG.setCount(file);}return(!LOG.write&&file||(file.open('a')&&file));};LOG.setFile=function(file,isCookie,overwrite){var bool,folder,fileName,suffix,newFileName,f,d,safeFileName;d=new Date();f=$.stack.split("\n")[0].replace(/^\[\(?/,'').replace(/\)?\]$/,'');if(f==~~f){f=$.fileName.replace(/[^\/]+\//g,'');}safeFileName=File.encode((isCookie?'/COOKIE_':'/LOG_')+f.replace(/^\//,'')+'_'+(1900+d.getYear())+(''+d).replace(/...(...)(..).+/,'_$1_$2')+(isCookie?'.txt':'.log'));if(file&&file.constructor.name=='String'){file=(file.match('/'))?new File(file):new File((logFolder||Folder.temp)+'/'+file);}if(file instanceof File){folder=file.parent;bool=folder.exists||folder.create();if(!bool)folder=Folder.temp;fileName=File.decode(file.name);suffix=fileName.match(/\.[^.]+$/);suffix=suffix?suffix[0]:'';fileName='/'+fileName;newFileName=fileName.replace(/\.[^.]+$/,'')+'_'+(+(new Date())+suffix);f=logNewFile(file,isCookie,overwrite);if(f)return f;f=logNewFile(new File(folder+newFileName),isCookie,overwrite);if(f)return f;f=logNewFile(new File(folder+safeFileName),isCookie,overwrite);if(f)return f;if(folder!=Folder.temp){f=logNewFile(new File(Folder.temp+fileName),isCookie,overwrite);if(f)return f;f=logNewFile(new File(Folder.temp+safeFileName),isCookie,overwrite);return f||new File(Folder.temp+safeFileName);}}return LOG.setFile(((logFile&&!isCookie)?new File(logFile):new File(Folder.temp+safeFileName)),isCookie,overwrite );};LOG.setCount=function(file){if(~~file===file){LOG.count=file;return LOG.count;}if(file===undefined){file=logFile;}if(file&&file.constructor===String){file=new File(file);}var logNumbers,contents;if(!file.length||!file.exists){LOG.count=1;return 1;}file.open('r');file.encoding='utf-8';file.seek(10000,2);contents='\n'+file.read();logNumbers=contents.match(/\n{0,3}\[\d+\] \[\w+\]+/g);if(logNumbers){logNumbers=+logNumbers[logNumbers.length-1].match(/\d+/)+1;file.close();LOG.count=logNumbers;return logNumbers;}if(file.length<10001){file.close();LOG.count=1;return 1;}file.seek(10000000,2);contents='\n'+file.read();logNumbers=contents.match(/\n{0,3}\[\d+\] \[\w+\]+/g);if(logNumbers){logNumbers=+logNumbers[logNumbers.length-1].match(/\d+/)+1;file.close();LOG.count=logNumbers;return logNumbers;}file.close();LOG.count=1;return 1;};LOG.setLevel=function(level){LOG.level=LOG.logDecodeLevel(level);return LOG.level;};LOG.setStatus=function(status){status=(''+status).toUpperCase();LOG.status=statuses[status]||status;return LOG.status;};LOG.cookie=function(file,level,overwrite,setLevel){var log,cookie;if(!file){file={file:file};}if(file&&(file.constructor===String||file.constructor===File)){file={file:file};}log=file;if(log.level===undefined){log.level=(level!==undefined)?level:'NONE';}if(log.overwrite===undefined){log.overwrite=(overwrite!==undefined)?overwrite:false;}if(log.setLevel===undefined){log.setLevel=(setLevel!==undefined)?setLevel:true;}setLevel=log.setLevel;overwrite=log.overwrite;level=log.level;file=log.file;file=LOG.setFile(file,true,overwrite);if(overwrite){file.write(level);}else{cookie=file.read();if(cookie.length){level=cookie;}else{file.write(level);}}file.close();if(setLevel){LOG.setLevel(level);}return{path:file,level:level};};LOG.args=function(args,funct,line){if(LOG.level>LOG_STATUS.FUNCTION)return;if(!(args&&(''+args.constructor).replace(/\s+/g,'')==='functionObject(){[nativecode]}'))return;if(!LOG.args.STRIP_COMMENTS){LOG.args.STRIP_COMMENTS=/((\/.*$)|(\/\*[\s\S]*?\*\/))/mg;}if(!LOG.args.ARGUMENT_NAMES){LOG.args.ARGUMENT_NAMES=/([^\s,]+)/g;}if(!LOG.args.OUTER_BRACKETS){LOG.args.OUTER_BRACKETS=/^\((.+)?\)$/;}if(!LOG.args.NEW_SOMETHING){LOG.args.NEW_SOMETHING=/^new \w+\((.+)?\)$/;}var functionString,argumentNames,stackInfo,report,functionName,arg,argsL,n,argName,argValue,argsTotal;if(funct===~~funct){line=funct;}if(!(funct instanceof Function)){funct=args.callee;}if(!(funct instanceof Function))return;functionName=funct.name;functionString=(''+funct).replace(LOG.args.STRIP_COMMENTS,'');argumentNames=functionString.slice(functionString.indexOf('(')+1,functionString.indexOf(')')).match(LOG.args.ARGUMENT_NAMES);argumentNames=argumentNames||[];report=[];report.push('--------------');report.push('Function Data:');report.push('--------------');report.push('Function Name:'+functionName);argsL=args.length;stackInfo=$.stack.split(/[\n\r]/);stackInfo.pop();stackInfo=stackInfo.join('\n ');report.push('Call stack:'+stackInfo);if(line){report.push('Function Line around:'+line);}report.push('Arguments Provided:'+argsL);report.push('Named Arguments:'+argumentNames.length);if(argumentNames.length){report.push('Arguments Names:'+argumentNames.join(','));}if(argsL){report.push('----------------');report.push('Argument Values:');report.push('----------------');}argsTotal=Math.max(argsL,argumentNames.length);for(n=0;n =argsL){argValue='NO VALUE PROVIDED';}else if(arg===undefined){argValue='undefined';}else if(arg===null){argValue='null';}else{argValue=arg.toSource().replace(LOG.args.OUTER_BRACKETS,'$1').replace(LOG.args.NEW_SOMETHING,'$1');}report.push((argName?argName:'arguments['+n+']')+':'+argValue);}report.push('');report=report.join('\n ');LOG(report,'f');return report;};LOG.stack=function(reverse){var st=$.stack.split('\n');st.pop();st.pop();if(reverse){st.reverse();}return LOG(st.join('\n '),'s');};LOG.values=function(values){var n,value,map=[];if(!(values instanceof Object||values instanceof Array)){return;}if(!LOG.values.OUTER_BRACKETS){LOG.values.OUTER_BRACKETS=/^\((.+)?\)$/;}if(!LOG.values.NEW_SOMETHING){LOG.values.NEW_SOMETHING=/^new \w+\((.+)?\)$/;}for(n in values){try{value=values[n];if(value===undefined){value='undefined';}else if(value===null){value='null';}else{value=value.toSource().replace(LOG.values.OUTER_BRACKETS,'$1').replace(LOG.values.NEW_SOMETHING,'$1');}}catch(e){value='\uD83D\uDEAB '+e;}map.push(n+':'+value);}if(map.length){map=map.join('\n ')+'\n ';return LOG(map,'v');}};LOG.reset=function(all){stack.length=0;LOG.count=1;if(all!==false){if(logFile instanceof File){logFile.close();}logFile=LOG.store=LOG.writeToFile=undefined;LOG.write=true;logFolder=Folder.temp;logTime=new Date();logPoint='After Log Reset';}};LOG.stopper=function(message){var newLogTime,t,m,newLogPoint;newLogTime=new Date();newLogPoint=(LOG.count!==undefined)?'LOG#'+LOG.count:'BEFORE LOG#1';LOG.time=t=newLogTime-logTime;if(message===false){return;}message=message||'Stopper start point';t=LOG.prettyTime(t);m=message+'\n '+'From '+logPoint+' to '+newLogPoint+' took '+t+' Starting '+logTime+' '+logTime.getMilliseconds()+'ms'+' Ending '+newLogTime+' '+newLogTime.getMilliseconds()+'ms';LOG(m,'st');logPoint=newLogPoint;logTime=newLogTime;return m;};LOG.start=function(message){var t=new Date();times.push([t,(message!==undefined)?message+'':'']);};LOG.stop=function(message){if(!times.length)return;message=(message)?message+' ':'';var nt,startLog,ot,om,td,m;nt=new Date();startLog=times.pop();ot=startLog[0];om=startLog[1];td=nt-ot;if(om.length){om+=' ';}m=om+'STARTED ['+ot+' '+ot.getMilliseconds()+'ms]\n '+message+'FINISHED ['+nt+' '+nt.getMilliseconds()+'ms]\n TOTAL TIME ['+LOG.prettyTime(td)+']';LOG(m,'ti');return m;};LOG.prettyTime=function(t){var h,m,s,ms;h=Math.floor(t / 3600000);m=Math.floor((t % 3600000)/ 60000);s=Math.floor((t % 60000)/ 1000);ms=t % 1000;t=(!t)?'<1ms':((h)?h+' hours ':'')+((m)?m+' minutes ':'')+((s)?s+' seconds ':'')+((ms&&(h||m||s))?'&':'')+((ms)?ms+'ms':'');return t;};LOG.get=function(){if(!stack.length)return 'THE LOG IS NOT SET TO STORE';var a=fetchLogLines(arguments);return a?'\n'+a.join('\n'):'NO LOGS AVAILABLE';};var fetchLogLines=function(){var args=arguments[0];if(!args.length)return stack;var c,n,l,a=[],ln,start,end,j,sl;l=args.length;sl=stack.length-1;n=0;for(c=0;c ln)?sl+ln+1:ln-1;if(ln>=0&&ln<=sl)a[n++]=stack[ln];}else if(ln instanceof Array&&ln.length===2){start=ln[0];end=ln[1];if(!(~~start===start&&~~end===end))continue;start=(0>start)?sl+start+1:start-1;end=(0>end)?sl+end+1:end-1;start=Math.max(Math.min(sl,start),0);end=Math.min(Math.max(end,0),sl);if(start<=end)for(j=start;j<=end;j++)a[n++]=stack[j];else for(j=start;j>=end;j--)a[n++]=stack[j];}}return(n)?a:false;};LOG.file=function(){return logFile;};LOG.openFolder=function(){if(logFolder)return logFolder.execute();};LOG.show=LOG.execute=function(){if(logFile)return logFile.execute();};LOG.close=function(){if(logFile)return logFile.close();};LOG.setFile(file);if(!$.summary.difference){$.summary.difference=function(){return $.summary().replace(/ *([0-9]+)([^ ]+)(\n?)/g,$.summary.updateSnapshot );};}if(!$.summary.updateSnapshot){$.summary.updateSnapshot=function(full,count,name,lf){var snapshot=$.summary.snapshot;count=Number(count);var prev=snapshot[name]?snapshot[name]:0;snapshot[name]=count;var diff=count-prev;if(diff===0)return "";return " ".substring(String(diff).length)+diff+" "+name+lf;};}if(!$.summary.snapshot){$.summary.snapshot=[];$.summary.difference();}$.gc();$.gc();$.summary.difference();LOG.sumDiff=function(message){$.gc();$.gc();var diff=$.summary.difference();if(diff.length<8){diff=' - NONE -';}if(message===undefined){message='';}message+=diff;return LOG('$.summary.difference():'+message,'v');};return LOG;}; var log = new LogFactory('myLog.log'); // =>; creates the new log factory - put full path where function getEnv(variable){ return $.getenv(variable); } function fileOpen(path){ return app.open(new File(path)); } function getLayerTypeWithName(layerName) { var type = 'NA'; var nameParts = layerName.split('_'); var namePrefix = nameParts[0]; namePrefix = namePrefix.toLowerCase(); switch (namePrefix) { case 'guide': case 'tl': case 'tr': case 'bl': case 'br': type = 'GUIDE'; break; case 'fg': type = 'FG'; break; case 'bg': type = 'BG'; break; case 'obj': default: type = 'OBJ'; break; } return type; } function getLayers() { /** * Get json representation of list of layers. * Much faster this way than in DOM traversal (2s vs 45s on same file) * * Format of single layer info: * id : number * name: string * group: boolean - true if layer is a group * parents:array - list of ids of parent groups, useful for selection * all children layers from parent layerSet (eg. group) * type: string - type of layer guessed from its name * visible:boolean - true if visible **/ if (documents.length == 0){ return '[]'; } var ref1 = new ActionReference(); ref1.putEnumerated(charIDToTypeID('Dcmn'), charIDToTypeID('Ordn'), charIDToTypeID('Trgt')); var count = executeActionGet(ref1).getInteger(charIDToTypeID('NmbL')); // get all layer names var layers = []; var layer = {}; var parents = []; for (var i = count; i >= 1; i--) { var layer = {}; var ref2 = new ActionReference(); ref2.putIndex(charIDToTypeID('Lyr '), i); var desc = executeActionGet(ref2); // Access layer index #i var layerSection = typeIDToStringID(desc.getEnumerationValue( stringIDToTypeID('layerSection'))); layer.id = desc.getInteger(stringIDToTypeID("layerID")); layer.name = desc.getString(stringIDToTypeID("name")); layer.color_code = typeIDToStringID(desc.getEnumerationValue(stringIDToTypeID('color'))); layer.group = false; layer.parents = parents.slice(); layer.type = getLayerTypeWithName(layer.name); layer.visible = desc.getBoolean(stringIDToTypeID("visible")); //log(" name: " + layer.name + " groupId " + layer.groupId + //" group " + layer.group); if (layerSection == 'layerSectionStart') { // Group start and end parents.push(layer.id); layer.group = true; } if (layerSection == 'layerSectionEnd') { parents.pop(); continue; } layers.push(JSON.stringify(layer)); } try{ var bck = activeDocument.backgroundLayer; layer.id = bck.id; layer.name = bck.name; layer.group = false; layer.parents = []; layer.type = 'background'; layer.visible = bck.visible; layers.push(JSON.stringify(layer)); }catch(e){ // do nothing, no background layer }; //log("layers " + layers); return '[' + layers + ']'; } function setVisible(layer_id, visibility){ /** * Sets particular 'layer_id' to 'visibility' if true > show **/ var desc = new ActionDescriptor(); var ref = new ActionReference(); ref.putIdentifier(stringIDToTypeID("layer"), layer_id); desc.putReference(stringIDToTypeID("null"), ref); executeAction(visibility?stringIDToTypeID("show"):stringIDToTypeID("hide"), desc, DialogModes.NO); } function getHeadline(){ /** * Returns headline of current document with metadata * **/ if (documents.length == 0){ return ''; } var headline = app.activeDocument.info.headline; return headline; } function isSaved(){ return app.activeDocument.saved; } function save(){ /** Saves active document **/ return app.activeDocument.save(); } function saveAs(output_path, ext, as_copy){ /** Exports scene to various formats * * Currently implemented: 'jpg', 'png', 'psd' * * output_path - escaped file path on local system * ext - extension for export * as_copy - create copy, do not overwrite * * */ var saveName = output_path; var saveOptions; if (ext == 'jpg'){ saveOptions = new JPEGSaveOptions(); saveOptions.quality = 12; saveOptions.embedColorProfile = true; saveOptions.formatOptions = FormatOptions.PROGRESSIVE; if(saveOptions.formatOptions == FormatOptions.PROGRESSIVE){ saveOptions.scans = 5}; saveOptions.matte = MatteType.NONE; } if (ext == 'png'){ saveOptions = new PNGSaveOptions(); saveOptions.interlaced = true; saveOptions.transparency = true; } if (ext == 'psd'){ saveOptions = null; return app.activeDocument.saveAs(new File(saveName)); } if (ext == 'psb'){ return savePSB(output_path); } return app.activeDocument.saveAs(new File(saveName), saveOptions, as_copy); } function getActiveDocumentName(){ /** * Returns file name of active document * */ if (documents.length == 0){ return null; } return app.activeDocument.name; } function getActiveDocumentFullName(){ /** * Returns file name of active document with file path. * activeDocument.fullName returns path in URI (eg /c/.. instead of c:/) * */ if (documents.length == 0){ return null; } var f = new File(app.activeDocument.fullName); var path = f.fsName; f.close(); return path; } function imprint(payload){ /** * Sets headline content of current document with metadata. Stores * information about assets created through Avalon. * Content accessible in PS through File > File Info * **/ app.activeDocument.info.headline = payload; } function getSelectedLayers(doc) { /** * Returns json representation of currently selected layers. * Works in three steps - 1) creates new group with selected layers * 2) traverses this group * 3) deletes newly created group, not needed * Bit weird, but Adobe.. **/ if (doc == null){ doc = app.activeDocument; } var selLayers = []; _grp = groupSelectedLayers(doc); var group = doc.activeLayer; var layers = group.layers; // // group is fake at this point // var itself_name = ''; // if (layers){ // itself_name = layers[0].name; // } for (var i = 0; i < layers.length; i++) { var layer = {}; layer.id = layers[i].id; layer.name = layers[i].name; long_names =_get_parents_names(group.parent, layers[i].name); var t = layers[i].kind; if ((typeof t !== 'undefined') && (layers[i].kind.toString() == 'LayerKind.NORMAL')){ layer.group = false; }else{ layer.group = true; } layer.long_name = long_names; selLayers.push(layer); } _undo(); return JSON.stringify(selLayers); }; function selectLayers(selectedLayers){ /** * Selects layers from list of ids **/ selectedLayers = JSON.parse(selectedLayers); var layers = new Array(); var id54 = charIDToTypeID( "slct" ); var desc12 = new ActionDescriptor(); var id55 = charIDToTypeID( "null" ); var ref9 = new ActionReference(); var existing_layers = JSON.parse(getLayers()); var existing_ids = []; for (var y = 0; y < existing_layers.length; y++){ existing_ids.push(existing_layers[y]["id"]); } for (var i = 0; i < selectedLayers.length; i++) { // a check to see if the id still exists var id = selectedLayers[i]; if(existing_ids.toString().indexOf(id)>=0){ layers[i] = charIDToTypeID( "Lyr " ); ref9.putIdentifier(layers[i], id); } } desc12.putReference( id55, ref9 ); var id58 = charIDToTypeID( "MkVs" ); desc12.putBoolean( id58, false ); executeAction( id54, desc12, DialogModes.NO ); } function groupSelectedLayers(doc, name) { /** * Groups selected layers into new group. * Returns json representation of Layer for server to consume * * Args: * doc(activeDocument) * name (str): new name of created group **/ if (doc == null){ doc = app.activeDocument; } var desc = new ActionDescriptor(); var ref = new ActionReference(); ref.putClass( stringIDToTypeID('layerSection') ); desc.putReference( charIDToTypeID('null'), ref ); var lref = new ActionReference(); lref.putEnumerated( charIDToTypeID('Lyr '), charIDToTypeID('Ordn'), charIDToTypeID('Trgt') ); desc.putReference( charIDToTypeID('From'), lref); executeAction( charIDToTypeID('Mk '), desc, DialogModes.NO ); var group = doc.activeLayer; if (name){ // Add special character to highlight group that will be published group.name = name; } var layer = {}; layer.id = group.id; layer.name = name; // keep name clean layer.group = true; layer.long_name = _get_parents_names(group, name); return JSON.stringify(layer); }; function importSmartObject(path, name, link){ /** * Creates new layer with an image from 'path' * * path: absolute path to loaded file * name: sets name of newly created laye * **/ var desc1 = new ActionDescriptor(); desc1.putPath( app.charIDToTypeID("null"), new File(path) ); link = link || false; if (link) { desc1.putBoolean( app.charIDToTypeID('Lnkd'), true ); } desc1.putEnumerated(app.charIDToTypeID("FTcs"), app.charIDToTypeID("QCSt"), app.charIDToTypeID("Qcsa")); var desc2 = new ActionDescriptor(); desc2.putUnitDouble(app.charIDToTypeID("Hrzn"), app.charIDToTypeID("#Pxl"), 0.0); desc2.putUnitDouble(app.charIDToTypeID("Vrtc"), app.charIDToTypeID("#Pxl"), 0.0); desc1.putObject(charIDToTypeID("Ofst"), charIDToTypeID("Ofst"), desc2); executeAction(charIDToTypeID("Plc " ), desc1, DialogModes.NO); var docRef = app.activeDocument var currentActivelayer = app.activeDocument.activeLayer; if (name){ currentActivelayer.name = name; } var layer = {} layer.id = currentActivelayer.id; layer.name = currentActivelayer.name; return JSON.stringify(layer); } function replaceSmartObjects(layer_id, path, name){ /** * Updates content of 'layer' with an image from 'path' * **/ var desc = new ActionDescriptor(); var ref = new ActionReference(); ref.putIdentifier(stringIDToTypeID("layer"), layer_id); desc.putReference(stringIDToTypeID("null"), ref); desc.putPath(charIDToTypeID('null'), new File(path) ); desc.putInteger(charIDToTypeID("PgNm"), 1); executeAction(stringIDToTypeID('placedLayerReplaceContents'), desc, DialogModes.NO ); var currentActivelayer = app.activeDocument.activeLayer; if (name){ currentActivelayer.name = name; } } function createGroup(name){ /** * Creates new group with a 'name' * Because of asynchronous nature, only group.id is available **/ group = app.activeDocument.layerSets.add(); // Add special character to highlight group that will be published group.name = name; return group.id; // only id available at this time :| } function deleteLayer(layer_id){ /*** * Deletes layer by its layer_id * * layer_id (int) **/ var d = new ActionDescriptor(); var r = new ActionReference(); r.putIdentifier(stringIDToTypeID("layer"), layer_id); d.putReference(stringIDToTypeID("null"), r); executeAction(stringIDToTypeID("delete"), d, DialogModes.NO); } function _undo() { executeAction(charIDToTypeID("undo", undefined, DialogModes.NO)); }; function savePSB(output_path){ /*** * Saves file as .psb to 'output_path' * * output_path (str) **/ var desc1 = new ActionDescriptor(); var desc2 = new ActionDescriptor(); desc2.putBoolean( stringIDToTypeID('maximizeCompatibility'), true ); desc1.putObject( charIDToTypeID('As '), charIDToTypeID('Pht8'), desc2 ); desc1.putPath( charIDToTypeID('In '), new File(output_path) ); desc1.putBoolean( charIDToTypeID('LwCs'), true ); executeAction( charIDToTypeID('save'), desc1, DialogModes.NO ); } function close(){ executeAction(stringIDToTypeID("quit"), undefined, DialogModes.NO ); } function renameLayer(layer_id, new_name){ /*** * Renames 'layer_id' to 'new_name' * * Via Action (fast) * * Args: * layer_id(int) * new_name(str) * * output_path (str) **/ doc = app.activeDocument; selectLayers('['+layer_id+']'); doc.activeLayer.name = new_name; } function _get_parents_names(layer, itself_name){ var long_names = [itself_name]; while (layer.parent){ if (layer.typename != "LayerSet"){ break; } long_names.push(layer.name); layer = layer.parent; } return long_names; } // triggers when panel is opened, good for debugging //log(getActiveDocumentName()); // log.show(); // var a = app.activeDocument.activeLayer; // log(a); //getSelectedLayers(); // importSmartObject("c:/projects/test.jpg", "a aaNewLayer", true); // log("dpc"); // replaceSmartObjects(153, "▼Jungle_imageTest_001", "c:/projects/test_project_test_asset_TestTask_v001.png"); ================================================ FILE: openpype/hosts/photoshop/api/extension/host/json.js ================================================ // json2.js // 2017-06-12 // Public Domain. // NO WARRANTY EXPRESSED OR IMPLIED. USE AT YOUR OWN RISK. // USE YOUR OWN COPY. IT IS EXTREMELY UNWISE TO LOAD CODE FROM SERVERS YOU DO // NOT CONTROL. // This file creates a global JSON object containing two methods: stringify // and parse. This file provides the ES5 JSON capability to ES3 systems. // If a project might run on IE8 or earlier, then this file should be included. // This file does nothing on ES5 systems. // JSON.stringify(value, replacer, space) // value any JavaScript value, usually an object or array. // replacer an optional parameter that determines how object // values are stringified for objects. It can be a // function or an array of strings. // space an optional parameter that specifies the indentation // of nested structures. If it is omitted, the text will // be packed without extra whitespace. If it is a number, // it will specify the number of spaces to indent at each // level. If it is a string (such as "\t" or " "), // it contains the characters used to indent at each level. // This method produces a JSON text from a JavaScript value. // When an object value is found, if the object contains a toJSON // method, its toJSON method will be called and the result will be // stringified. A toJSON method does not serialize: it returns the // value represented by the name/value pair that should be serialized, // or undefined if nothing should be serialized. The toJSON method // will be passed the key associated with the value, and this will be // bound to the value. // For example, this would serialize Dates as ISO strings. // Date.prototype.toJSON = function (key) { // function f(n) { // // Format integers to have at least two digits. // return (n < 10) // ? "0" + n // : n; // } // return this.getUTCFullYear() + "-" + // f(this.getUTCMonth() + 1) + "-" + // f(this.getUTCDate()) + "T" + // f(this.getUTCHours()) + ":" + // f(this.getUTCMinutes()) + ":" + // f(this.getUTCSeconds()) + "Z"; // }; // You can provide an optional replacer method. It will be passed the // key and value of each member, with this bound to the containing // object. The value that is returned from your method will be // serialized. If your method returns undefined, then the member will // be excluded from the serialization. // If the replacer parameter is an array of strings, then it will be // used to select the members to be serialized. It filters the results // such that only members with keys listed in the replacer array are // stringified. // Values that do not have JSON representations, such as undefined or // functions, will not be serialized. Such values in objects will be // dropped; in arrays they will be replaced with null. You can use // a replacer function to replace those with JSON values. // JSON.stringify(undefined) returns undefined. // The optional space parameter produces a stringification of the // value that is filled with line breaks and indentation to make it // easier to read. // If the space parameter is a non-empty string, then that string will // be used for indentation. If the space parameter is a number, then // the indentation will be that many spaces. // Example: // text = JSON.stringify(["e", {pluribus: "unum"}]); // // text is '["e",{"pluribus":"unum"}]' // text = JSON.stringify(["e", {pluribus: "unum"}], null, "\t"); // // text is '[\n\t"e",\n\t{\n\t\t"pluribus": "unum"\n\t}\n]' // text = JSON.stringify([new Date()], function (key, value) { // return this[key] instanceof Date // ? "Date(" + this[key] + ")" // : value; // }); // // text is '["Date(---current time---)"]' // JSON.parse(text, reviver) // This method parses a JSON text to produce an object or array. // It can throw a SyntaxError exception. // The optional reviver parameter is a function that can filter and // transform the results. It receives each of the keys and values, // and its return value is used instead of the original value. // If it returns what it received, then the structure is not modified. // If it returns undefined then the member is deleted. // Example: // // Parse the text. Values that look like ISO date strings will // // be converted to Date objects. // myData = JSON.parse(text, function (key, value) { // var a; // if (typeof value === "string") { // a = // /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2}(?:\.\d*)?)Z$/.exec(value); // if (a) { // return new Date(Date.UTC( // +a[1], +a[2] - 1, +a[3], +a[4], +a[5], +a[6] // )); // } // return value; // } // }); // myData = JSON.parse( // "[\"Date(09/09/2001)\"]", // function (key, value) { // var d; // if ( // typeof value === "string" // && value.slice(0, 5) === "Date(" // && value.slice(-1) === ")" // ) { // d = new Date(value.slice(5, -1)); // if (d) { // return d; // } // } // return value; // } // ); // This is a reference implementation. You are free to copy, modify, or // redistribute. /*jslint eval, for, this */ /*property JSON, apply, call, charCodeAt, getUTCDate, getUTCFullYear, getUTCHours, getUTCMinutes, getUTCMonth, getUTCSeconds, hasOwnProperty, join, lastIndex, length, parse, prototype, push, replace, slice, stringify, test, toJSON, toString, valueOf */ // Create a JSON object only if one does not already exist. We create the // methods in a closure to avoid creating global variables. if (typeof JSON !== "object") { JSON = {}; } (function () { "use strict"; var rx_one = /^[\],:{}\s]*$/; var rx_two = /\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g; var rx_three = /"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g; var rx_four = /(?:^|:|,)(?:\s*\[)+/g; var rx_escapable = /[\\"\u0000-\u001f\u007f-\u009f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g; var rx_dangerous = /[\u0000\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g; function f(n) { // Format integers to have at least two digits. return (n < 10) ? "0" + n : n; } function this_value() { return this.valueOf(); } if (typeof Date.prototype.toJSON !== "function") { Date.prototype.toJSON = function () { return isFinite(this.valueOf()) ? ( this.getUTCFullYear() + "-" + f(this.getUTCMonth() + 1) + "-" + f(this.getUTCDate()) + "T" + f(this.getUTCHours()) + ":" + f(this.getUTCMinutes()) + ":" + f(this.getUTCSeconds()) + "Z" ) : null; }; Boolean.prototype.toJSON = this_value; Number.prototype.toJSON = this_value; String.prototype.toJSON = this_value; } var gap; var indent; var meta; var rep; function quote(string) { // If the string contains no control characters, no quote characters, and no // backslash characters, then we can safely slap some quotes around it. // Otherwise we must also replace the offending characters with safe escape // sequences. rx_escapable.lastIndex = 0; return rx_escapable.test(string) ? "\"" + string.replace(rx_escapable, function (a) { var c = meta[a]; return typeof c === "string" ? c : "\\u" + ("0000" + a.charCodeAt(0).toString(16)).slice(-4); }) + "\"" : "\"" + string + "\""; } function str(key, holder) { // Produce a string from holder[key]. var i; // The loop counter. var k; // The member key. var v; // The member value. var length; var mind = gap; var partial; var value = holder[key]; // If the value has a toJSON method, call it to obtain a replacement value. if ( value && typeof value === "object" && typeof value.toJSON === "function" ) { value = value.toJSON(key); } // If we were called with a replacer function, then call the replacer to // obtain a replacement value. if (typeof rep === "function") { value = rep.call(holder, key, value); } // What happens next depends on the value's type. switch (typeof value) { case "string": return quote(value); case "number": // JSON numbers must be finite. Encode non-finite numbers as null. return (isFinite(value)) ? String(value) : "null"; case "boolean": case "null": // If the value is a boolean or null, convert it to a string. Note: // typeof null does not produce "null". The case is included here in // the remote chance that this gets fixed someday. return String(value); // If the type is "object", we might be dealing with an object or an array or // null. case "object": // Due to a specification blunder in ECMAScript, typeof null is "object", // so watch out for that case. if (!value) { return "null"; } // Make an array to hold the partial results of stringifying this object value. gap += indent; partial = []; // Is the value an array? if (Object.prototype.toString.apply(value) === "[object Array]") { // The value is an array. Stringify every element. Use null as a placeholder // for non-JSON values. length = value.length; for (i = 0; i < length; i += 1) { partial[i] = str(i, value) || "null"; } // Join all of the elements together, separated with commas, and wrap them in // brackets. v = partial.length === 0 ? "[]" : gap ? ( "[\n" + gap + partial.join(",\n" + gap) + "\n" + mind + "]" ) : "[" + partial.join(",") + "]"; gap = mind; return v; } // If the replacer is an array, use it to select the members to be stringified. if (rep && typeof rep === "object") { length = rep.length; for (i = 0; i < length; i += 1) { if (typeof rep[i] === "string") { k = rep[i]; v = str(k, value); if (v) { partial.push(quote(k) + ( (gap) ? ": " : ":" ) + v); } } } } else { // Otherwise, iterate through all of the keys in the object. for (k in value) { if (Object.prototype.hasOwnProperty.call(value, k)) { v = str(k, value); if (v) { partial.push(quote(k) + ( (gap) ? ": " : ":" ) + v); } } } } // Join all of the member texts together, separated with commas, // and wrap them in braces. v = partial.length === 0 ? "{}" : gap ? "{\n" + gap + partial.join(",\n" + gap) + "\n" + mind + "}" : "{" + partial.join(",") + "}"; gap = mind; return v; } } // If the JSON object does not yet have a stringify method, give it one. if (typeof JSON.stringify !== "function") { meta = { // table of character substitutions "\b": "\\b", "\t": "\\t", "\n": "\\n", "\f": "\\f", "\r": "\\r", "\"": "\\\"", "\\": "\\\\" }; JSON.stringify = function (value, replacer, space) { // The stringify method takes a value and an optional replacer, and an optional // space parameter, and returns a JSON text. The replacer can be a function // that can replace values, or an array of strings that will select the keys. // A default replacer method can be provided. Use of the space parameter can // produce text that is more easily readable. var i; gap = ""; indent = ""; // If the space parameter is a number, make an indent string containing that // many spaces. if (typeof space === "number") { for (i = 0; i < space; i += 1) { indent += " "; } // If the space parameter is a string, it will be used as the indent string. } else if (typeof space === "string") { indent = space; } // If there is a replacer, it must be a function or an array. // Otherwise, throw an error. rep = replacer; if (replacer && typeof replacer !== "function" && ( typeof replacer !== "object" || typeof replacer.length !== "number" )) { throw new Error("JSON.stringify"); } // Make a fake root object containing our value under the key of "". // Return the result of stringifying the value. return str("", {"": value}); }; } // If the JSON object does not yet have a parse method, give it one. if (typeof JSON.parse !== "function") { JSON.parse = function (text, reviver) { // The parse method takes a text and an optional reviver function, and returns // a JavaScript value if the text is a valid JSON text. var j; function walk(holder, key) { // The walk method is used to recursively walk the resulting structure so // that modifications can be made. var k; var v; var value = holder[key]; if (value && typeof value === "object") { for (k in value) { if (Object.prototype.hasOwnProperty.call(value, k)) { v = walk(value, k); if (v !== undefined) { value[k] = v; } else { delete value[k]; } } } } return reviver.call(holder, key, value); } // Parsing happens in four stages. In the first stage, we replace certain // Unicode characters with escape sequences. JavaScript handles many characters // incorrectly, either silently deleting them, or treating them as line endings. text = String(text); rx_dangerous.lastIndex = 0; if (rx_dangerous.test(text)) { text = text.replace(rx_dangerous, function (a) { return ( "\\u" + ("0000" + a.charCodeAt(0).toString(16)).slice(-4) ); }); } // In the second stage, we run the text against regular expressions that look // for non-JSON patterns. We are especially concerned with "()" and "new" // because they can cause invocation, and "=" because it can cause mutation. // But just to be safe, we want to reject all unexpected forms. // We split the second stage into 4 regexp operations in order to work around // crippling inefficiencies in IE's and Safari's regexp engines. First we // replace the JSON backslash pairs with "@" (a non-JSON character). Second, we // replace all simple value tokens with "]" characters. Third, we delete all // open brackets that follow a colon or comma or that begin the text. Finally, // we look to see that the remaining characters are only whitespace or "]" or // "," or ":" or "{" or "}". If that is so, then the text is safe for eval. if ( rx_one.test( text .replace(rx_two, "@") .replace(rx_three, "]") .replace(rx_four, "") ) ) { // In the third stage we use the eval function to compile the text into a // JavaScript structure. The "{" operator is subject to a syntactic ambiguity // in JavaScript: it can begin a block or an object literal. We wrap the text // in parens to eliminate the ambiguity. j = eval("(" + text + ")"); // In the optional fourth stage, we recursively walk the new structure, passing // each name/value pair to a reviver function for possible transformation. return (typeof reviver === "function") ? walk({"": j}, "") : j; } // If the text is not JSON parseable, then a SyntaxError is thrown. throw new SyntaxError("JSON.parse"); }; } }()); ================================================ FILE: openpype/hosts/photoshop/api/extension/index.html ================================================ ================================================ FILE: openpype/hosts/photoshop/api/launch_logic.py ================================================ import os import subprocess import collections import asyncio from wsrpc_aiohttp import ( WebSocketRoute, WebSocketAsync ) from qtpy import QtCore from openpype.lib import Logger, StringTemplate from openpype.pipeline import ( registered_host, Anatomy, ) from openpype.pipeline.workfile import ( get_workfile_template_key_from_context, get_last_workfile, ) from openpype.pipeline.template_data import get_template_data_with_names from openpype.tools.utils import host_tools from openpype.tools.adobe_webserver.app import WebServerTool from openpype.pipeline.context_tools import change_current_context from openpype.client import get_asset_by_name from .ws_stub import PhotoshopServerStub log = Logger.get_logger(__name__) class ConnectionNotEstablishedYet(Exception): pass class MainThreadItem: """Structure to store information about callback in main thread. Item should be used to execute callback in main thread which may be needed for execution of Qt objects. Item store callback (callable variable), arguments and keyword arguments for the callback. Item hold information about it's process. """ not_set = object() def __init__(self, callback, *args, **kwargs): self._done = False self._exception = self.not_set self._result = self.not_set self._callback = callback self._args = args self._kwargs = kwargs @property def done(self): return self._done @property def exception(self): return self._exception @property def result(self): return self._result def execute(self): """Execute callback and store its result. Method must be called from main thread. Item is marked as `done` when callback execution finished. Store output of callback of exception information when callback raises one. """ log.debug("Executing process in main thread") if self.done: log.warning("- item is already processed") return log.info("Running callback: {}".format(str(self._callback))) try: result = self._callback(*self._args, **self._kwargs) self._result = result except Exception as exc: self._exception = exc finally: self._done = True def stub(): """ Convenience function to get server RPC stub to call methods directed for host (Photoshop). It expects already created connection, started from client. Currently created when panel is opened (PS: Window>Extensions>Avalon) :return: where functions could be called from """ ps_stub = PhotoshopServerStub() if not ps_stub.client: raise ConnectionNotEstablishedYet("Connection is not created yet") return ps_stub def show_tool_by_name(tool_name): kwargs = {} if tool_name == "loader": kwargs["use_context"] = True host_tools.show_tool_by_name(tool_name, **kwargs) class ProcessLauncher(QtCore.QObject): route_name = "Photoshop" _main_thread_callbacks = collections.deque() def __init__(self, subprocess_args): self._subprocess_args = subprocess_args self._log = None super(ProcessLauncher, self).__init__() # Keep track if launcher was already started self._started = False self._process = None self._websocket_server = None start_process_timer = QtCore.QTimer() start_process_timer.setInterval(100) loop_timer = QtCore.QTimer() loop_timer.setInterval(200) start_process_timer.timeout.connect(self._on_start_process_timer) loop_timer.timeout.connect(self._on_loop_timer) self._start_process_timer = start_process_timer self._loop_timer = loop_timer @property def log(self): if self._log is None: self._log = Logger.get_logger( "{}-launcher".format(self.route_name) ) return self._log @property def websocket_server_is_running(self): if self._websocket_server is not None: return self._websocket_server.is_running return False @property def is_process_running(self): if self._process is not None: return self._process.poll() is None return False @property def is_host_connected(self): """Returns True if connected, False if app is not running at all.""" if not self.is_process_running: return False try: _stub = stub() if _stub: return True except Exception: pass return None @classmethod def execute_in_main_thread(cls, callback, *args, **kwargs): item = MainThreadItem(callback, *args, **kwargs) cls._main_thread_callbacks.append(item) return item def start(self): if self._started: return self.log.info("Started launch logic of Photoshop") self._started = True self._start_process_timer.start() def exit(self): """ Exit whole application. """ if self._start_process_timer.isActive(): self._start_process_timer.stop() if self._loop_timer.isActive(): self._loop_timer.stop() if self._websocket_server is not None: self._websocket_server.stop() if self._process: self._process.kill() self._process.wait() QtCore.QCoreApplication.exit() def _on_loop_timer(self): # TODO find better way and catch errors # Run only callbacks that are in queue at the moment cls = self.__class__ for _ in range(len(cls._main_thread_callbacks)): if cls._main_thread_callbacks: item = cls._main_thread_callbacks.popleft() item.execute() if not self.is_process_running: self.log.info("Host process is not running. Closing") self.exit() elif not self.websocket_server_is_running: self.log.info("Websocket server is not running. Closing") self.exit() def _on_start_process_timer(self): # TODO add try except validations for each part in this method # Start server as first thing if self._websocket_server is None: self._init_server() return # TODO add waiting time # Wait for webserver if not self.websocket_server_is_running: return # Start application process if self._process is None: self._start_process() self.log.info("Waiting for host to connect") return # TODO add waiting time # Wait until host is connected if self.is_host_connected: self._start_process_timer.stop() self._loop_timer.start() elif ( not self.is_process_running or not self.websocket_server_is_running ): self.exit() def _init_server(self): if self._websocket_server is not None: return self.log.debug( "Initialization of websocket server for host communication" ) self._websocket_server = websocket_server = WebServerTool() if websocket_server.port_occupied( websocket_server.host_name, websocket_server.port ): self.log.info( "Server already running, sending actual context and exit." ) asyncio.run(websocket_server.send_context_change(self.route_name)) self.exit() return # Add Websocket route websocket_server.add_route("*", "/ws/", WebSocketAsync) # Add after effects route to websocket handler print("Adding {} route".format(self.route_name)) WebSocketAsync.add_route( self.route_name, PhotoshopRoute ) self.log.info("Starting websocket server for host communication") websocket_server.start_server() def _start_process(self): if self._process is not None: return self.log.info("Starting host process") try: self._process = subprocess.Popen( self._subprocess_args, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL ) except Exception: self.log.info("exce", exc_info=True) self.exit() class PhotoshopRoute(WebSocketRoute): """ One route, mimicking external application (like Harmony, etc). All functions could be called from client. 'do_notify' function calls function on the client - mimicking notification after long running job on the server or similar """ instance = None def init(self, **kwargs): # Python __init__ must be return "self". # This method might return anything. log.debug("someone called Photoshop route") self.instance = self return kwargs # server functions async def ping(self): log.debug("someone called Photoshop route ping") # This method calls function on the client side # client functions async def set_context(self, project, asset, task): """ Sets 'project' and 'asset' to envs, eg. setting context. Opens last workile from that context if exists. Args: project (str) asset (str) task (str """ log.info("Setting context change") log.info(f"project {project} asset {asset} task {task}") asset_doc = get_asset_by_name(project, asset) change_current_context(asset_doc, task) last_workfile_path = self._get_last_workfile_path(project, asset, task) if last_workfile_path and os.path.exists(last_workfile_path): ProcessLauncher.execute_in_main_thread( lambda: stub().open(last_workfile_path)) async def read(self): log.debug("photoshop.read client calls server server calls " "photoshop client") return await self.socket.call('photoshop.read') # panel routes for tools async def workfiles_route(self): self._tool_route("workfiles") async def loader_route(self): self._tool_route("loader") async def publish_route(self): self._tool_route("publisher") async def sceneinventory_route(self): self._tool_route("sceneinventory") async def experimental_tools_route(self): self._tool_route("experimental_tools") def _tool_route(self, _tool_name): """The address accessed when clicking on the buttons.""" ProcessLauncher.execute_in_main_thread(show_tool_by_name, _tool_name) # Required return statement. return "nothing" def _get_last_workfile_path(self, project_name, asset_name, task_name): """Returns last workfile path if exists""" host = registered_host() host_name = "photoshop" template_key = get_workfile_template_key_from_context( asset_name, task_name, host_name, project_name=project_name ) anatomy = Anatomy(project_name) data = get_template_data_with_names( project_name, asset_name, task_name, host_name ) data["root"] = anatomy.roots file_template = anatomy.templates[template_key]["file"] # Define saving file extension extensions = host.get_workfile_extensions() folder_template = anatomy.templates[template_key]["folder"] work_root = StringTemplate.format_strict_template( folder_template, data ) last_workfile_path = get_last_workfile( work_root, file_template, data, extensions, True ) return last_workfile_path ================================================ FILE: openpype/hosts/photoshop/api/lib.py ================================================ import os import sys import contextlib import traceback from openpype.lib import env_value_to_bool, Logger from openpype.modules import ModulesManager from openpype.pipeline import install_host from openpype.tools.utils import host_tools from openpype.tools.utils import get_openpype_qt_app from openpype.tests.lib import is_in_tests from .launch_logic import ProcessLauncher, stub log = Logger.get_logger(__name__) def safe_excepthook(*args): traceback.print_exception(*args) def main(*subprocess_args): from openpype.hosts.photoshop.api import PhotoshopHost host = PhotoshopHost() install_host(host) sys.excepthook = safe_excepthook # coloring in StdOutBroker os.environ["OPENPYPE_LOG_NO_COLORS"] = "False" app = get_openpype_qt_app() app.setQuitOnLastWindowClosed(False) launcher = ProcessLauncher(subprocess_args) launcher.start() if env_value_to_bool("HEADLESS_PUBLISH"): manager = ModulesManager() webpublisher_addon = manager["webpublisher"] launcher.execute_in_main_thread( webpublisher_addon.headless_publish, log, "ClosePS", is_in_tests() ) elif env_value_to_bool("AVALON_PHOTOSHOP_WORKFILES_ON_LAUNCH", default=True): launcher.execute_in_main_thread( host_tools.show_workfiles, save=env_value_to_bool("WORKFILES_SAVE_AS") ) sys.exit(app.exec_()) @contextlib.contextmanager def maintained_selection(): """Maintain selection during context.""" selection = stub().get_selected_layers() try: yield selection finally: stub().select_layers(selection) @contextlib.contextmanager def maintained_visibility(layers=None): """Maintain visibility during context. Args: layers (list) of PSItem (used for caching) """ visibility = {} if not layers: layers = stub().get_layers() for layer in layers: visibility[layer.id] = layer.visible try: yield finally: for layer in layers: stub().set_visible(layer.id, visibility[layer.id]) pass ================================================ FILE: openpype/hosts/photoshop/api/pipeline.py ================================================ import os from qtpy import QtWidgets import pyblish.api from openpype.lib import register_event_callback, Logger from openpype.pipeline import ( register_loader_plugin_path, register_creator_plugin_path, AVALON_CONTAINER_ID, ) from openpype.host import ( HostBase, IWorkfileHost, ILoadHost, IPublishHost ) from openpype.pipeline.load import any_outdated_containers from openpype.hosts.photoshop import PHOTOSHOP_HOST_DIR from openpype.tools.utils import get_openpype_qt_app from . import lib log = Logger.get_logger(__name__) PLUGINS_DIR = os.path.join(PHOTOSHOP_HOST_DIR, "plugins") PUBLISH_PATH = os.path.join(PLUGINS_DIR, "publish") LOAD_PATH = os.path.join(PLUGINS_DIR, "load") CREATE_PATH = os.path.join(PLUGINS_DIR, "create") INVENTORY_PATH = os.path.join(PLUGINS_DIR, "inventory") class PhotoshopHost(HostBase, IWorkfileHost, ILoadHost, IPublishHost): name = "photoshop" def install(self): """Install Photoshop-specific functionality needed for integration. This function is called automatically on calling `api.install(photoshop)`. """ log.info("Installing OpenPype Photoshop...") pyblish.api.register_host("photoshop") pyblish.api.register_plugin_path(PUBLISH_PATH) register_loader_plugin_path(LOAD_PATH) register_creator_plugin_path(CREATE_PATH) register_event_callback("application.launched", on_application_launch) def current_file(self): try: full_name = lib.stub().get_active_document_full_name() if full_name and full_name != "null": return os.path.normpath(full_name).replace("\\", "/") except Exception: pass return None def work_root(self, session): return os.path.normpath(session["AVALON_WORKDIR"]).replace("\\", "/") def open_workfile(self, filepath): lib.stub().open(filepath) return True def save_workfile(self, filepath=None): _, ext = os.path.splitext(filepath) lib.stub().saveAs(filepath, ext[1:], True) def get_current_workfile(self): return self.current_file() def workfile_has_unsaved_changes(self): if self.current_file(): return not lib.stub().is_saved() return False def get_workfile_extensions(self): return [".psd", ".psb"] def get_containers(self): return ls() def get_context_data(self): """Get stored values for context (validation enable/disable etc)""" meta = _get_stub().get_layers_metadata() for item in meta: if item.get("id") == "publish_context": item.pop("id") return item return {} def update_context_data(self, data, changes): """Store value needed for context""" item = data item["id"] = "publish_context" _get_stub().imprint(item["id"], item) def list_instances(self): """List all created instances to publish from current workfile. Pulls from File > File Info Returns: (list) of dictionaries matching instances format """ stub = _get_stub() if not stub: return [] instances = [] layers_meta = stub.get_layers_metadata() if layers_meta: for instance in layers_meta: if instance.get("id") == "pyblish.avalon.instance": instances.append(instance) return instances def remove_instance(self, instance): """Remove instance from current workfile metadata. Updates metadata of current file in File > File Info and removes icon highlight on group layer. Args: instance (dict): instance representation from subsetmanager model """ stub = _get_stub() if not stub: return inst_id = instance.get("instance_id") or instance.get("uuid") # legacy if not inst_id: log.warning("No instance identifier for {}".format(instance)) return stub.remove_instance(inst_id) if instance.get("members"): item = stub.get_layer(instance["members"][0]) if item: stub.rename_layer(item.id, item.name.replace(stub.PUBLISH_ICON, '')) def check_inventory(): if not any_outdated_containers(): return # Warn about outdated containers. _app = get_openpype_qt_app() message_box = QtWidgets.QMessageBox() message_box.setIcon(QtWidgets.QMessageBox.Warning) msg = "There are outdated containers in the scene." message_box.setText(msg) message_box.exec_() def on_application_launch(): check_inventory() def ls(): """Yields containers from active Photoshop document This is the host-equivalent of api.ls(), but instead of listing assets on disk, it lists assets already loaded in Photoshop; once loaded they are called 'containers' Yields: dict: container """ try: stub = lib.stub() # only after Photoshop is up except lib.ConnectionNotEstablishedYet: print("Not connected yet, ignoring") return if not stub.get_active_document_name(): return layers_meta = stub.get_layers_metadata() # minimalize calls to PS for layer in stub.get_layers(): data = stub.read(layer, layers_meta) # Skip non-tagged layers. if not data: continue # Filter to only containers. if "container" not in data["id"]: continue # Append transient data data["objectName"] = layer.name.replace(stub.LOADED_ICON, '') data["layer"] = layer yield data def _get_stub(): """Handle pulling stub from PS to run operations on host Returns: (PhotoshopServerStub) or None """ try: stub = lib.stub() # only after Photoshop is up except lib.ConnectionNotEstablishedYet: print("Not connected yet, ignoring") return if not stub.get_active_document_name(): return return stub def containerise( name, namespace, layer, context, loader=None, suffix="_CON" ): """Imprint layer with metadata Containerisation enables a tracking of version, author and origin for loaded assets. Arguments: name (str): Name of resulting assembly namespace (str): Namespace under which to host container layer (PSItem): Layer to containerise context (dict): Asset information loader (str, optional): Name of loader used to produce this container. suffix (str, optional): Suffix of container, defaults to `_CON`. Returns: container (str): Name of container assembly """ layer.name = name + suffix data = { "schema": "openpype:container-2.0", "id": AVALON_CONTAINER_ID, "name": name, "namespace": namespace, "loader": str(loader), "representation": str(context["representation"]["_id"]), "members": [str(layer.id)] } stub = lib.stub() stub.imprint(layer.id, data) return layer def cache_and_get_instances(creator): """Cache instances in shared data. Storing all instances as a list as legacy instances might be still present. Args: creator (Creator): Plugin which would like to get instances from host. Returns: List[]: list of all instances stored in metadata """ shared_key = "openpype.photoshop.instances" if shared_key not in creator.collection_shared_data: creator.collection_shared_data[shared_key] = \ creator.host.list_instances() return creator.collection_shared_data[shared_key] ================================================ FILE: openpype/hosts/photoshop/api/plugin.py ================================================ import re from openpype.pipeline import LoaderPlugin from .launch_logic import stub def get_unique_layer_name(layers, asset_name, subset_name): """ Gets all layer names and if 'asset_name_subset_name' is present, it increases suffix by 1 (eg. creates unique layer name - for Loader) Args: layers (list) of dict with layers info (name, id etc.) asset_name (string): subset_name (string): Returns: (string): name_00X (without version) """ name = "{}_{}".format(asset_name, subset_name) names = {} for layer in layers: layer_name = re.sub(r'_\d{3}$', '', layer.name) if layer_name in names.keys(): names[layer_name] = names[layer_name] + 1 else: names[layer_name] = 1 occurrences = names.get(name, 0) return "{}_{:0>3d}".format(name, occurrences + 1) class PhotoshopLoader(LoaderPlugin): @staticmethod def get_stub(): return stub() ================================================ FILE: openpype/hosts/photoshop/api/ws_stub.py ================================================ """ Stub handling connection from server to client. Used anywhere solution is calling client methods. """ import json import attr from wsrpc_aiohttp import WebSocketAsync from openpype.tools.adobe_webserver.app import WebServerTool @attr.s class PSItem(object): """ Object denoting layer or group item in PS. Each item is created in PS by any Loader, but contains same fields, which are being used in later processing. """ # metadata id = attr.ib() # id created by AE, could be used for querying name = attr.ib() # name of item group = attr.ib(default=None) # item type (footage, folder, comp) parents = attr.ib(factory=list) visible = attr.ib(default=True) type = attr.ib(default=None) # all imported elements, single for members = attr.ib(factory=list) long_name = attr.ib(default=None) color_code = attr.ib(default=None) # color code of layer instance_id = attr.ib(default=None) @property def clean_name(self): """Returns layer name without publish icon highlight Returns: (str) """ return (self.name.replace(PhotoshopServerStub.PUBLISH_ICON, '') .replace(PhotoshopServerStub.LOADED_ICON, '')) class PhotoshopServerStub: """ Stub for calling function on client (Photoshop js) side. Expects that client is already connected (started when avalon menu is opened). 'self.websocketserver.call' is used as async wrapper """ PUBLISH_ICON = '\u2117 ' LOADED_ICON = '\u25bc' def __init__(self): self.websocketserver = WebServerTool.get_instance() self.client = self.get_client() @staticmethod def get_client(): """ Return first connected client to WebSocket TODO implement selection by Route :return: client """ clients = WebSocketAsync.get_clients() client = None if len(clients) > 0: key = list(clients.keys())[0] client = clients.get(key) return client def open(self, path): """Open file located at 'path' (local). Args: path(string): file path locally Returns: None """ self.websocketserver.call( self.client.call('Photoshop.open', path=path) ) def read(self, layer, layers_meta=None): """Parses layer metadata from Headline field of active document. Args: layer: (PSItem) layers_meta: full list from Headline (for performance in loops) Returns: (dict) of layer metadata stored in PS file Example: { 'id': 'pyblish.avalon.container', 'loader': 'ImageLoader', 'members': ['64'], 'name': 'imageMainMiddle', 'namespace': 'Hero_imageMainMiddle_001', 'representation': '6203dc91e80934d9f6ee7d96', 'schema': 'openpype:container-2.0' } """ if layers_meta is None: layers_meta = self.get_layers_metadata() for layer_meta in layers_meta: layer_id = layer_meta.get("uuid") # legacy if layer_meta.get("members"): layer_id = layer_meta["members"][0] if str(layer.id) == str(layer_id): return layer_meta print("Unable to find layer metadata for {}".format(layer.id)) def imprint(self, item_id, data, all_layers=None, items_meta=None): """Save layer metadata to Headline field of active document Stores metadata in format: [{ "active":true, "subset":"imageBG", "family":"image", "id":"pyblish.avalon.instance", "asset":"Town", "uuid": "8" }] - for created instances OR [{ "schema": "openpype:container-2.0", "id": "pyblish.avalon.instance", "name": "imageMG", "namespace": "Jungle_imageMG_001", "loader": "ImageLoader", "representation": "5fbfc0ee30a946093c6ff18a", "members": [ "40" ] }] - for loaded instances Args: item_id (str): data(string): json representation for single layer all_layers (list of PSItem): for performance, could be injected for usage in loop, if not, single call will be triggered items_meta(string): json representation from Headline (for performance - provide only if imprint is in loop - value should be same) Returns: None """ if not items_meta: items_meta = self.get_layers_metadata() # json.dumps writes integer values in a dictionary to string, so # anticipating it here. item_id = str(item_id) is_new = True result_meta = [] for item_meta in items_meta: if ((item_meta.get('members') and item_id == str(item_meta.get('members')[0])) or item_meta.get("instance_id") == item_id): is_new = False if data: item_meta.update(data) result_meta.append(item_meta) else: result_meta.append(item_meta) if is_new: result_meta.append(data) # Ensure only valid ids are stored. if not all_layers: all_layers = self.get_layers() layer_ids = [layer.id for layer in all_layers] cleaned_data = [] for item in result_meta: if item.get("members"): if int(item["members"][0]) not in layer_ids: continue cleaned_data.append(item) payload = json.dumps(cleaned_data, indent=4) self.websocketserver.call( self.client.call('Photoshop.imprint', payload=payload) ) def get_layers(self): """Returns JSON document with all(?) layers in active document. Returns: Format of tuple: { 'id':'123', 'name': 'My Layer 1', 'type': 'GUIDE'|'FG'|'BG'|'OBJ' 'visible': 'true'|'false' """ res = self.websocketserver.call( self.client.call('Photoshop.get_layers') ) return self._to_records(res) def get_layer(self, layer_id): """ Returns PSItem for specific 'layer_id' or None if not found Args: layer_id (string): unique layer id, stored in 'uuid' field Returns: (PSItem) or None """ layers = self.get_layers() for layer in layers: if str(layer.id) == str(layer_id): return layer def get_layers_in_layers(self, layers): """Return all layers that belong to layers (might be groups). Args: layers
: Returns:
""" parent_ids = set([lay.id for lay in layers]) return self._get_layers_in_layers(parent_ids) def get_layers_in_layers_ids(self, layers_ids, layers=None): """Return all layers that belong to layers (might be groups). Args: layers_ids
layers
: Returns:
""" parent_ids = set(layers_ids) return self._get_layers_in_layers(parent_ids, layers) def _get_layers_in_layers(self, parent_ids, layers=None): if not layers: layers = self.get_layers() all_layers = layers ret = [] for layer in all_layers: parents = set(layer.parents) if len(parent_ids & parents) > 0: ret.append(layer) if layer.id in parent_ids: ret.append(layer) return ret def create_group(self, name): """Create new group (eg. LayerSet) Returns: