diff --git a/.clang-format b/.clang-format index 369a912de..3f6b84b74 100644 --- a/.clang-format +++ b/.clang-format @@ -8,9 +8,9 @@ AlignConsecutiveDeclarations: true AlignEscapedNewlines: Right AlignOperands: true AlignTrailingComments: true -# AllowAllArgumentsOnNextLine: true -AllowAllParametersOfDeclarationOnNextLine: true -# AllowAllConstructorInitializersOnNextLine: true +AllowAllArgumentsOnNextLine: false +AllowAllParametersOfDeclarationOnNextLine: false +AllowAllConstructorInitializersOnNextLine: false AllowShortBlocksOnASingleLine: false AllowShortCaseLabelsOnASingleLine: false AllowShortFunctionsOnASingleLine: InlineOnly @@ -39,7 +39,7 @@ BraceWrapping: SplitEmptyFunction: false SplitEmptyRecord: false SplitEmptyNamespace: false -BreakBeforeBinaryOperators: None +BreakBeforeBinaryOperators: NonAssignment BreakBeforeBraces: Custom BreakBeforeInheritanceComma: false BreakInheritanceList: BeforeColon diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 741f37899..9e7a1ef84 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -18,5 +18,5 @@ If no new tests are introduced as part of this PR, note the tests that are provi diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml new file mode 100644 index 000000000..f2f784945 --- /dev/null +++ b/.github/workflows/docs.yml @@ -0,0 +1,105 @@ +name: docs + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + +jobs: + check-links: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + with: + submodules: 'recursive' + + - uses: actions/setup-python@v4.3.0 + with: + python-version: '3.10' + + - name: Create virtualenv + run: python3 -m venv .venv + + - name: Install dependencies + run: | + source .venv/bin/activate + python -m pip install . + python -m pip install -r docs/requirements.txt + + - name: Linkcheck + working-directory: docs + run: | + source ../.venv/bin/activate + + set +e + make linkcheck + exit_code=$? + + set -e + + if [ $exit_code -eq 0 ]; then + echo -e "\n\n=================\nAll links are valid!" + + echo "# :heavy_check_mark: Sphinx links" >> $GITHUB_STEP_SUMMARY + echo "All links are valid!" >> $GITHUB_STEP_SUMMARY + else + echo -e "\n\n=================\nFound broken links. Look at the build logs.\n" + + echo "# :x: Sphinx links" >> $GITHUB_STEP_SUMMARY + echo "Found broken links. Look at the build logs for additional information." >> $GITHUB_STEP_SUMMARY + echo '```' >> $GITHUB_STEP_SUMMARY + cat _build/linkcheck/output.txt >> $GITHUB_STEP_SUMMARY + echo '```' >> $GITHUB_STEP_SUMMARY + fi + + exit $exit_code + + check-warnings: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + with: + submodules: 'recursive' + + - uses: actions/setup-python@v4.3.0 + with: + python-version: '3.10' + + - name: Create virtualenv + run: python3 -m venv .venv + + - name: Install dependencies + run: | + source .venv/bin/activate + python -m pip install . + python -m pip install -r docs/requirements.txt + + - name: Check warnings/errors + working-directory: docs + run: | + source ../.venv/bin/activate + + set +e + make htmlstrict + + exit_code=$? + + set -e + + if [ $exit_code -eq 0 ]; then + echo -e "\n\n=================\nNo warnings or errors detected!" + echo "# :heavy_check_mark: Sphinx warnings/errors" >> $GITHUB_STEP_SUMMARY + echo "No errors or warnings detected!" >> $GITHUB_STEP_SUMMARY + else + echo -e "\n\n=================\nWarnings and or errors detected; See the summary bellow:\n" + cat _build/htmlstrict/output.txt + + echo "# :x: Sphinx warnings/errors" >> $GITHUB_STEP_SUMMARY + echo "Found some warnings or errors:" >> $GITHUB_STEP_SUMMARY + echo '```' >> $GITHUB_STEP_SUMMARY + cat _build/htmlstrict/output.txt >> $GITHUB_STEP_SUMMARY + echo '```' >> $GITHUB_STEP_SUMMARY + fi + + exit $exit_code diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index bf556c40a..2b1ba9d61 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -15,6 +15,10 @@ on: pull_request: branches: [ main ] +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + jobs: cpp_build: runs-on: ${{ matrix.os }} @@ -35,6 +39,7 @@ jobs: OTIO_BUILD_CONFIG: Release OTIO_BUILD_DIR: ${{ github.workspace }}/build OTIO_INSTALL_DIR: ${{ github.workspace }}/install + OTIO_CONSUMER_TEST_BUILD_DIR: ${{ github.workspace }}/consumertest steps: - uses: actions/checkout@v3 @@ -67,7 +72,7 @@ jobs: # \todo Should the Codecov web pages show the results of the C++ or Python tests? # - name: Upload coverage to Codecov # if: matrix.os == env.GH_COV_OS && github.actor != env.GH_DEPENDABOT -# uses: codecov/codecov-action@v2.1.0 +# uses: codecov/codecov-action@v3.1.1 # with: # files: ${{ env.OTIO_BUILD_DIR }}/coverage.filtered.info # flags: unittests @@ -77,13 +82,27 @@ jobs: run: | cd ${{ env.OTIO_BUILD_DIR }} cmake --build . --target install --config ${{ env.OTIO_BUILD_CONFIG }} + - name: Consumer tests + run: | + cmake -E make_directory ${{ env.OTIO_CONSUMER_TEST_BUILD_DIR }} + cd ${{ env.OTIO_CONSUMER_TEST_BUILD_DIR }} + cmake ${{ github.workspace }}/tests/consumer -DCMAKE_PREFIX_PATH=${{ env.OTIO_INSTALL_DIR }} py_build_test: runs-on: ${{ matrix.os }} strategy: matrix: os: [ubuntu-latest, windows-latest, macos-latest] - python-version: ['2.7', '3.7', '3.8', '3.9', '3.10'] + python-version: ['3.7', '3.8', '3.9', '3.10', '3.11'] + include: + - { os: ubuntu-latest, shell: bash } + - { os: macos-latest, shell: bash } + - { os: windows-latest, shell: pwsh } + - { os: windows-latest, shell: msys2, python-version: 'mingw64' } + + defaults: + run: + shell: '${{ matrix.shell }} {0}' env: OTIO_CXX_COVERAGE_BUILD: ON @@ -93,20 +112,31 @@ jobs: - uses: actions/checkout@v3 with: submodules: 'recursive' + - name: Set up MSYS2 + if: matrix.python-version == 'mingw64' + uses: msys2/setup-msys2@v2 + with: + msystem: mingw64 + install: >- + mingw-w64-x86_64-python + mingw-w64-x86_64-python-pip + mingw-w64-x86_64-gcc + mingw-w64-x86_64-cmake + make + git - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v3 + if: matrix.python-version != 'mingw64' + uses: actions/setup-python@v4.3.0 with: python-version: ${{ matrix.python-version }} - name: Install coverage dependency if: matrix.python-version == env.GH_COV_PY && matrix.os == env.GH_COV_OS && github.actor != env.GH_DEPENDABOT run: | + echo 'OTIO_CXX_DEBUG_BUILD=1' >> $GITHUB_ENV sudo apt-get install lcov - name: Install python build dependencies run: | - python -m pip install --upgrade pip setuptools wheel flake8>=3.5 check-manifest - - name: install mock for python 2.7 tests only - if: matrix.python-version == 2.7 - run: python -m pip install --upgrade mock + python -m pip install --upgrade pip setuptools wheel "flake8>=3.5" check-manifest - name: Run check-manifest and lint check run: make ci-prebuild - name: Build and Install @@ -121,7 +151,7 @@ jobs: run: make lcov - name: Upload coverage to Codecov if: matrix.python-version == env.GH_COV_PY && matrix.os == env.GH_COV_OS && github.actor != env.GH_DEPENDABOT - uses: codecov/codecov-action@v2.1.0 + uses: codecov/codecov-action@v3.1.1 with: flags: py-unittests name: py-opentimelineio-codecov @@ -133,47 +163,21 @@ jobs: strategy: matrix: os: [ubuntu-latest, windows-latest, macos-latest] - python-build: ['cp27*', 'cp37*', 'cp38*', 'cp39*', 'cp310*'] + python-build: ['cp37*', 'cp38*', 'cp39*', 'cp310*', 'cp311*'] steps: - uses: actions/checkout@v3 - # cibuildwheel 1.12.0 gates Python 2.7 wheels builds - # by using two environment variables, DISTUTILS_USE_SDK and MSSdk. - # https://cibuildwheel.readthedocs.io/en/1.x/cpp_standards/#windows-and-python-27 - # Note that normally these are used by setuptools/distutils, but in our case - # they are really just used for cibuildwheel as we don't use any of the - # setuptools/distutils build tools. Our builds are entirely handled - # by CMake. CMake is able to find the right toolchain, thanks to - # the -A argument that we specify in the setup.py to set the - # target platform (x86, x64, etc). - - name: Set Windows Python 2.7 environment variables - if: matrix.python-build == 'cp27*' && runner.os == 'Windows' - shell: bash - run: | - echo "DISTUTILS_USE_SDK=1" >> $GITHUB_ENV - echo "MSSdk=1" >> $GITHUB_ENV - - - name: Build wheels (Python 2.7) - if: matrix.python-build == 'cp27*' - # cibuildwheel 1.12.0 is the last release that supported Python 2.7. - uses: pypa/cibuildwheel@v1.12.0 - with: - output-dir: wheelhouse - env: - CIBW_BUILD: ${{ matrix.python-build }} - - name: Build wheels (Python 3) - uses: pypa/cibuildwheel@v2.3.1 - if: matrix.python-build != 'cp27*' + uses: pypa/cibuildwheel@v2.11.1 with: output-dir: wheelhouse env: CIBW_BUILD: ${{ matrix.python-build }} CIBW_SKIP: '*musllinux*' - CIBW_MANYLINUX_X86_64_IMAGE: manylinux2010 - CIBW_MANYLINUX_I686_IMAGE: manylinux2010 + CIBW_MANYLINUX_X86_64_IMAGE: manylinux2014 + CIBW_MANYLINUX_I686_IMAGE: manylinux2014 - - uses: actions/upload-artifact@v2 + - uses: actions/upload-artifact@v3 with: name: wheels path: ./wheelhouse/*.whl @@ -186,7 +190,7 @@ jobs: with: submodules: 'recursive' - - uses: actions/setup-python@v3 + - uses: actions/setup-python@v4.3.0 - name: Install pypa/build run: python -m pip install build --user @@ -194,7 +198,7 @@ jobs: - name: Generate sdist run: python -m build -s . - - uses: actions/upload-artifact@v2 + - uses: actions/upload-artifact@v3 with: name: sdist path: dist diff --git a/.gitignore b/.gitignore index 9cf64c081..dd2e1c7e4 100644 --- a/.gitignore +++ b/.gitignore @@ -10,10 +10,16 @@ htmlcov .vscode/ xcuserdata/ .venv/ +.cache # Pycharm metadata .idea/ # These files are generated, don't put them into source control docs/api +docs/_build .tox +cpp_cov_html/ +lcov_html_report/ +*.so +*.pyd diff --git a/.readthedocs.yml b/.readthedocs.yml index 77fddc6b8..f0873ba44 100644 --- a/.readthedocs.yml +++ b/.readthedocs.yml @@ -2,19 +2,16 @@ version: 2 build: - image: latest + os: "ubuntu-20.04" + tools: + python: "3.10" python: - version: 3.7 install: - - method: pip - path: . - extra_requirements: - - cmake + - method: pip + path: . + - requirements: docs/requirements.txt submodules: include: all recursive: true - -conda: - environment: readthedocs-conda.yml diff --git a/CMakeLists.txt b/CMakeLists.txt index 5c6b58796..3f7d70ce1 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -3,11 +3,11 @@ cmake_minimum_required(VERSION 3.18.2) #------------------------------------------------------------------------------ -# Project Meta data +# Project Metadata # TODO: read this information from a configuration file, here, and in setup.py set(OTIO_VERSION_MAJOR "0") -set(OTIO_VERSION_MINOR "14") +set(OTIO_VERSION_MINOR "16") set(OTIO_VERSION_PATCH "0") set(OTIO_VERSION ${OTIO_VERSION_MAJOR}.${OTIO_VERSION_MINOR}.${OTIO_VERSION_PATCH}) @@ -114,18 +114,6 @@ else() # Note that this has no effect on Windows. set(CMAKE_CXX_VISIBILITY_PRESET hidden) set(CMAKE_VISIBILITY_INLINES_HIDDEN 1) - - if(MSVC AND Python_VERSION_MAJOR VERSION_LESS 3) - # Statically link run-time library (vcruntime and msvcp) - # See https://docs.microsoft.com/en-us/cpp/build/reference/md-mt-ld-use-run-time-library?view=msvc-160 - # This allows us to compile OTIO bindings with a newer MSVC version - # than the one used by the interpreter where OTIO will be installed. - # This is only required for Python < 3 because only these are - # compiled with an older compiler (9.0). CPython 3.5+ uses at least - # Visual C++ 14.X. - # See https://wiki.python.org/moin/WindowsCompilers#Which_Microsoft_Visual_C.2B-.2B-_compiler_to_use_with_a_specific_Python_version_.3F - add_compile_options(/MT) - endif() endif() endif() @@ -181,11 +169,7 @@ set_property(GLOBAL PROPERTY USE_FOLDERS ON) #------------------------------------------------------------------------------ # Fetch or refresh submodules if requested # -# fetching submodules does not work in Travis, so override the OTIO_AUTOMATIC_SUBMODULES option -# TODO: Travis is no longer used for CI of OpenTimelineIO, so the ENV var that overrides -# the automatic submodule feature should be renamed. - -if (OTIO_AUTOMATIC_SUBMODULES AND NOT DEFINED ENV{TRAVIS}) +if (OTIO_AUTOMATIC_SUBMODULES) # make sure that git submodules are up to date when building find_package(Git QUIET) if (GIT_FOUND) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index a5f65d2b4..370c46640 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -15,19 +15,27 @@ Also see the [GOVERNANCE](GOVERNANCE.md) document for details on the rules and r The OpenTimelineIO Project Committers (alphabetically by last name) are: +- Daniel Flehner Heen ([apetrynet on github](https://github.com/apetrynet)) +- Jeff Hodges ([jhodges10 on github](https://github.com/jhodges10)) +- Darby Johnston ([darbyjohnston on github](https://github.com/darbyjohnston)) - Joshua Minor ([jminor on github](https://github.com/jminor)) +- Jean-Christophe Morin ([JeanChristopheMorinPerso on github](https://github.com/JeanChristopheMorinPerso)) +- Roger Nelson ([rogernelson on github](https://github.com/rogernelson)) - Nick Porcino ([meshula on github](https://github.com/meshula)) - Eric Reinecke ([reinecke on github](https://github.com/reinecke)) - Stephan Steinbach ([ssteinbach on github](https://github.com/ssteinbach)) ## Contributor License Agreement -Before contributing code to OpenTimelineIO, we ask that you sign a Contributor License Agreement (CLA). At the root of the repo you can find the two possible CLAs: +Before contributing code to OpenTimelineIO, we ask that you sign a Contributor License Agreement (CLA). +When you create a pull request, the Linux Foundation's EasyCLA system will guide you through the process of signing the CLA. -* [OTIO_CLA_Corporate.pdf](https://github.com/PixarAnimationStudios/OpenTimelineIO/raw/main/OTIO_CLA_Corporate.pdf): please sign this one for corporate use -* [OTIO_CLA_Individual.pdf](https://github.com/PixarAnimationStudios/OpenTimelineIO/raw/main/OTIO_CLA_Individual.pdf): please sign this one if you're an individual contributor +If you are unable to use the EasyCLA system, you can send a signed CLA to `opentimelineio-tsc@aswf.io` (please make sure to include your github username) and wait for confirmation that we've received it. -Once your CLA is signed, send it to `opentimelineio-tsc@aswf.io` (please make sure to include your github username) and wait for confirmation that we've received it. After that, you can submit pull requests. +Here are the two possible CLAs: + +* [OTIO_CLA_Corporate.pdf](https://github.com/AcademySoftwareFoundation/OpenTimelineIO/raw/main/OTIO_CLA_Corporate.pdf): please sign this one for corporate use +* [OTIO_CLA_Individual.pdf](https://github.com/AcademySoftwareFoundation/OpenTimelineIO/raw/main/OTIO_CLA_Individual.pdf): please sign this one if you're an individual contributor ## Coding Conventions Please follow the coding convention and style in each file and in each library when adding new files. @@ -49,7 +57,7 @@ Add the primary OpenTimelineIO repo as upstream to make it easier to update your ```bash cd OpenTimelineIO -git remote add upstream https://github.com/PixarAnimationStudios/OpenTimelineIO.git +git remote add upstream https://github.com/AcademySoftwareFoundation/OpenTimelineIO.git ``` Now you fetch the latest changes from the OpenTimelineIO repo like this: diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index ccfdb68f4..0842825f9 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -5,9 +5,11 @@ If you know of anyone missing from this list, please contact us: https://lists.a * Aditya Rana ([Viraj-Rana008](https://github.com/Viraj-Rana008)) * Alex Schworer ([schworer](https://github.com/schworer)) * Alex Widener ([boredstiff](https://github.com/boredstiff)) +* Andrew Moore ([andrewmoore-nz](https://github.com/andrewmoore-nz)) * Ashley Whetter ([AWhetter](https://github.com/AWhetter)) * Bas Hesen ([bashesenaxis](https://github.com/bashesenaxis)) * Bonnie Tai ([bonpix](https://github.com/bonpix)) +* Cameron Kerr ([camkerr](https://github.com/camkerr)) * Daniel Flehner Heen ([apetrynet](https://github.com/apetrynet)) * Darby Johnston ([darbyjohnston](https://github.com/darbyjohnston)) * Darwyn Peachey ([peachey](https://github.com/peachey)) @@ -15,15 +17,17 @@ If you know of anyone missing from this list, please contact us: https://lists.a * Ed Caspersen ([repsac](https://github.com/repsac)) * Emile Labrosse ([elabrosseRodeofx](https://github.com/elabrosseRodeofx)) * Eric ([eric-with-a-c](https://github.com/eric-with-a-c)) +* Eric Desruisseaux ([desruie](https://github.com/desruie)) * Eric Reinecke ([reinecke](https://github.com/reinecke)) -* Félix Bourbonnais ([BadSingleton](https://github.com/BadSingleton)) -* Félix David ([Tilix4](https://github.com/Tilix4)) * Flavian Liger ([flavienliger](https://github.com/flavienliger)) * Fredrik Brännbacka ([smaragden](https://github.com/smaragden)) * Freeson Wang ([freesonluxo](https://github.com/freesonluxo)) +* Félix Bourbonnais ([BadSingleton](https://github.com/BadSingleton)) +* Félix David ([Tilix4](https://github.com/Tilix4)) * Henry Wilkes ([hwilkes-igalia](https://github.com/hwilkes-igalia)) * Jean-Christophe Morin ([JeanChristopheMorinPerso](https://github.com/JeanChristopheMorinPerso)) * Jeffrey Barendse ([IOjeffrey](https://github.com/IOjeffrey)) +* jlskuz ([jlskuz](https://github.com/jlskuz)) * John Mertic ([jmertic](https://github.com/jmertic)) * Jon Morley ([rogergodspeed](https://github.com/rogergodspeed)) * Jonathan Hearn ([splidje](https://github.com/splidje)) @@ -32,11 +36,14 @@ If you know of anyone missing from this list, please contact us: https://lists.a * Julian Yu-Chung Chen ([jchen9](https://github.com/jchen9)) * Karthik Ramesh Iyer ([KarthikRIyer](https://github.com/KarthikRIyer)) * Laura Savidge ([lsavidge](https://github.com/lsavidge)) +* Mark Reid ([markreidvfx](https://github.com/markreidvfx)) * Matt Johnson ([mattyjams](https://github.com/mattyjams)) +* Michael Dolan ([michdolan](https://github.com/michdolan)) * Michael Jefferies ([michaeljefferies](https://github.com/michaeljefferies)) * Mike Koetter ([mikekoetter](https://github.com/mikekoetter)) * Mike Mahony ([mikemahony](https://github.com/mikemahony)) * Nick Porcino ([meshula](https://github.com/meshula)) +* Pedro Labonia ([pedrolabonia](https://github.com/pedrolabonia)) * Robyn Rindge ([avrata](https://github.com/avrata)) * Roger Nelson ([rogernelson](https://github.com/rogernelson)) * Sasha Aleshchenko ([TheBigSasha](https://github.com/TheBigSasha)) @@ -46,10 +53,14 @@ If you know of anyone missing from this list, please contact us: https://lists.a * Simran Spiller ([Simran-B](https://github.com/Simran-B)) * Stefan Schulze ([stefanschulze](https://github.com/stefanschulze)) * Stephan Steinbach ([ssteinbach](https://github.com/ssteinbach)) +* Stéphane Deverly ([gplsteph](https://github.com/gplsteph)) * Thibault Saunier ([thiblahute](https://github.com/thiblahute)) * Thierry Dervieux-Lecocq ([tdervieux](https://github.com/tdervieux)) +* ThomasWilshaw ([ThomasWilshaw](https://github.com/ThomasWilshaw)) * Tim Lehr ([timlehr](https://github.com/timlehr)) * Troy James Sobotka ([sobotka](https://github.com/sobotka)) * Tuan Truong ([tuan-huy-truong](https://github.com/tuan-huy-truong)) +* Utsab Saha ([utsab](https://github.com/utsab)) * Valerio Viperino ([vvzen](https://github.com/vvzen)) * Vincent Pinon ([vpinon](https://github.com/vpinon)) +* Visaj Nirav Shah ([visajshah](https://github.com/visajshah)) \ No newline at end of file diff --git a/GOVERNANCE.md b/GOVERNANCE.md index 27d39246a..852b26229 100644 --- a/GOVERNANCE.md +++ b/GOVERNANCE.md @@ -106,7 +106,9 @@ project at ASWF TAC meetings. (alphabetical by last name) +* Jeff Hodges - Adobe * Joshua Minor (chair) - Pixar +* Roger Nelson - Autodesk * Nick Porcino - Pixar * Eric Reinecke - Netflix * Stephan Steinbach - Pixar diff --git a/LICENSE.txt b/LICENSE.txt index 50aee0c0d..261eeb9e9 100644 --- a/LICENSE.txt +++ b/LICENSE.txt @@ -1,6 +1,6 @@ - - Modified Apache 2.0 License - + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION @@ -136,9 +136,9 @@ with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor - and its affiliates, except as required to comply with Section 4(c) of - the License and to reproduce the content of the NOTICE file. + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each @@ -173,4 +173,29 @@ incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. -OpenTimelineIO bundles ffmpeg_burnins.py, which is available under the MIT License. For details contrib/adapters. + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/MANIFEST.in b/MANIFEST.in index be62154de..567cb83e0 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -2,7 +2,7 @@ include README.md CHANGELOG.md LICENSE.txt NOTICE.txt CMakeLists.txt recursive-include examples * recursive-include contrib * recursive-include src * - +recursive-include tests * prune .github prune docs prune doxygen @@ -12,7 +12,6 @@ exclude .readthedocs.yml exclude readthedocs-conda.yml exclude .codecov.yml exclude .gitlab-ci.yml -exclude .travis.yml exclude *.pdf exclude CODE_OF_CONDUCT.md exclude CONTRIBUTING.md @@ -22,10 +21,12 @@ exclude contrib/opentimelineio_contrib/adapters/Makefile exclude Makefile exclude */.DS_Store exclude .clang-format +exclude OTIO_VERSION.json +global-exclude *.pyc +global-exclude *.so +global-exclude *.pyd -prune contrib/opentimelineio_contrib/adapters/tests prune maintainers -prune tests prune tsc prune src/deps/pybind11/tools/clang prune src/deps/rapidjson/thirdparty diff --git a/Makefile b/Makefile index 1dc597e3a..6b1e8224a 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,5 @@ .PHONY: coverage test test_first_fail clean autopep8 lint doc-html \ - python-version wheel manifest lcov + python-version wheel manifest lcov lcov-html lcov-reset # Special definition to handle Make from stripping newlines define newline @@ -20,7 +20,6 @@ $(ccblue) pip install -e .[dev]$(newline)$(ccend) endef # variables -DOC_OUTPUT_DIR ?= /var/tmp/otio-docs MAKE_PROG ?= make # external programs @@ -34,6 +33,10 @@ CLANG_FORMAT_PROG := $(shell command -v clang-format 2> /dev/null) # AUTOPEP8_PROG := $(shell command -v autopep8 2> /dev/null) TEST_ARGS= +GIT = git +GITSTATUS := $(shell git diff-index --quiet HEAD . 1>&2 2> /dev/null; echo $$?) + + ifeq ($(VERBOSE), 1) TEST_ARGS:=-v endif @@ -96,17 +99,26 @@ ifndef OTIO_CXX_BUILD_TMP_DIR C++ coverage will not work, because intermediate build products will \ not be found.) endif - lcov --capture -b . --directory ${OTIO_CXX_BUILD_TMP_DIR} \ + lcov --rc lcov_branch_coverage=1 --capture -b . --directory ${OTIO_CXX_BUILD_TMP_DIR} \ --output-file=${OTIO_CXX_BUILD_TMP_DIR}/coverage.info -q cat ${OTIO_CXX_BUILD_TMP_DIR}/coverage.info | sed "s/SF:.*src/SF:src/g"\ > ${OTIO_CXX_BUILD_TMP_DIR}/coverage.filtered.info - lcov --remove ${OTIO_CXX_BUILD_TMP_DIR}/coverage.filtered.info '/usr/*' \ + lcov --rc lcov_branch_coverage=1 --remove ${OTIO_CXX_BUILD_TMP_DIR}/coverage.filtered.info '/usr/*' \ --output-file=${OTIO_CXX_BUILD_TMP_DIR}/coverage.filtered.info -q - lcov --remove ${OTIO_CXX_BUILD_TMP_DIR}/coverage.filtered.info '*/deps/*' \ + lcov --rc lcov_branch_coverage=1 --remove ${OTIO_CXX_BUILD_TMP_DIR}/coverage.filtered.info '*/deps/*' \ --output-file=${OTIO_CXX_BUILD_TMP_DIR}/coverage.filtered.info -q rm ${OTIO_CXX_BUILD_TMP_DIR}/coverage.info lcov --list ${OTIO_CXX_BUILD_TMP_DIR}/coverage.filtered.info + +lcov-html: lcov + @echo -e "$(ccgreen)Generating C++ HTML coverage report...$(ccend)" + genhtml --quiet --branch-coverage --output-directory lcov_html_report ${OTIO_CXX_BUILD_TMP_DIR}/coverage.filtered.info + +lcov-reset: + @echo "$(tput setaf -Txterm 2)Resetting C++ coverage...$(tput sgr0)" + lcov -b . --directory ${OTIO_CXX_BUILD_TMP_DIR} --zerocounters + # run all the unit tests, stopping at the first failure test_first_fail: python-version @python -m unittest discover -s tests --failfast @@ -117,6 +129,7 @@ ifdef COV_PROG endif @${MAKE_PROG} -C contrib/opentimelineio_contrib/adapters clean VERBOSE=$(VERBOSE) rm -vf *.whl + @cd docs; ${MAKE_PROG} clean # conform all files to pep8 -- WILL CHANGE FILES IN PLACE # autopep8: @@ -182,13 +195,112 @@ doc-plugins: doc-plugins-update: @python src/py-opentimelineio/opentimelineio/console/autogen_plugin_documentation.py -o docs/tutorials/otio-plugins.md --public-only --sanitized-paths +# build the CORE_VERSION_MAP cpp file +version-map: + @python src/py-opentimelineio/opentimelineio/console/autogen_version_map.py -i src/opentimelineio/CORE_VERSION_MAP.last.cpp --dryrun + +version-map-update: + @echo "updating the CORE_VERSION_MAP..." + @python src/py-opentimelineio/opentimelineio/console/autogen_version_map.py -i src/opentimelineio/CORE_VERSION_MAP.last.cpp -o src/opentimelineio/CORE_VERSION_MAP.cpp + # generate documentation in html doc-html: @# if you just want to build the docs yourself outside of RTD - @echo "Writing documentation to $(DOC_OUTPUT_DIR), set variable DOC_OUTPUT_DIR to change output directory." - @cd docs ; sphinx-build -j8 -E -b html -d $(DOC_OUTPUT_DIR)/doctrees . $(DOC_OUTPUT_DIR)/html + @cd docs; ${MAKE_PROG} html doc-cpp: @cd doxygen ; doxygen config/dox_config ; cd .. @echo "wrote doxygen output to: doxygen/output/html/index.html" +# release related targets +confirm-release-intent: +ifndef OTIO_DO_RELEASE + $(error \ + "If you are sure you want to perform a release, set OTIO_DO_RELEASE=1") +endif + @echo "Starting release process..." + +check-git-status: +ifneq ($(GITSTATUS), 0) + $(error \ + "Git repository is dirty, cannot create release. Run 'git status' \ + for more info") +endif + @echo "Git status is clean, ready to proceed with release." + +verify-license: + @echo "Verifying licenses in files..." + @python maintainers/verify_license.py -s . + +fix-license: + @python maintainers/verify_license.py -s . -f + +freeze-ci-versions: + @echo "freezing CI versions..." + @python maintainers/freeze_ci_versions.py -f + +unfreeze-ci-versions: + @echo "unfreezing CI versions..." + @python maintainers/freeze_ci_versions.py -u + +# needs to happen _before_ version-map-update so that version in +# CORE_VERSION_MAP does not have the .dev1 suffix at release time +remove-dev-suffix: + @echo "Removing .dev1 suffix" + @python maintainers/remove_dev_suffix.py -r + +check-github-token: +ifndef OTIO_RELEASE_GITHUB_TOKEN + $(error \ + OTIO_RELEASE_GITHUB_TOKEN is not set, unable to update contributors) +endif + +update-contributors: check-github-token + @echo "Updating CONTRIBUTORS.md..." + @python maintainers/fetch_contributors.py \ + --repo AcademySoftwareFoundation/OpenTimelineIO \ + --token $(OTIO_RELEASE_GITHUB_TOKEN) + +dev-python-install: + @python setup.py install + +# make target for preparing a release candidate +release: \ + confirm-release-intent \ + check-git-status \ + check-github-token \ + verify-license \ + freeze-ci-versions \ + remove-dev-suffix \ + format \ + dev-python-install \ + version-map-update \ + test-core \ + update-contributors + @echo "Release is ready. Commit, push and open a PR!" + +# targets for creating a new version (after making a release, to start the next +# development cycle) +bump-otio-minor-version: + @python maintainers/bump_version_number.py -i minor + +shuffle-core-version-map: + @cp -f src/opentimelineio/CORE_VERSION_MAP.cpp \ + src/opentimelineio/CORE_VERSION_MAP.last.cpp + @echo "set the current version map as the next one" + +add-dev-suffix: + @echo "Adding .dev1 suffix" + @python maintainers/remove_dev_suffix.py -a + +# make target for starting a new version (after a release is completed) +start-dev-new-minor-version: \ + check-git-status \ + unfreeze-ci-versions \ + bump-otio-minor-version \ + shuffle-core-version-map \ + add-dev-suffix \ + dev-python-install \ + version-map-update \ + test-core + @echo "New version made. Commit, push and open a PR!" diff --git a/OTIO_CLA_Corporate.pdf b/OTIO_CLA_Corporate.pdf index e2f19ad4b..b285186b3 100644 Binary files a/OTIO_CLA_Corporate.pdf and b/OTIO_CLA_Corporate.pdf differ diff --git a/OTIO_CLA_Individual.pdf b/OTIO_CLA_Individual.pdf index 174107259..c5d13faef 100644 Binary files a/OTIO_CLA_Individual.pdf and b/OTIO_CLA_Individual.pdf differ diff --git a/OTIO_VERSION.json b/OTIO_VERSION.json new file mode 100644 index 000000000..42f1608fc --- /dev/null +++ b/OTIO_VERSION.json @@ -0,0 +1 @@ +{"version": ["0", "16", "0"]} \ No newline at end of file diff --git a/README.md b/README.md index b9931641b..3b4b7dae5 100644 --- a/README.md +++ b/README.md @@ -3,22 +3,23 @@ OpenTimelineIO [![OpenTimelineIO](docs/_static/OpenTimelineIO@3xDark.png)](http://opentimeline.io) ============== -[![Supported VFX Platform Versions](https://img.shields.io/badge/vfx%20platform-2018--2021-lightgrey.svg)](http://www.vfxplatform.com/) -![Supported Versions](https://img.shields.io/badge/python-2.7%2C%203.7%2C%203.8%2C%203.9%2C%203.10-blue) -[![Build Status](https://github.com/PixarAnimationStudios/OpenTimelineIO/actions/workflows/python-package.yml/badge.svg)](https://github.com/PixarAnimationStudios/OpenTimelineIO/actions/workflows/python-package.yml) -[![codecov](https://codecov.io/gh/PixarAnimationStudios/OpenTimelineIO/branch/main/graph/badge.svg)](https://codecov.io/gh/PixarAnimationStudios/OpenTimelineIO) +[![Supported VFX Platform Versions](https://img.shields.io/badge/vfx%20platform-2020--2023-lightgrey.svg)](http://www.vfxplatform.com/) +![Supported Versions](https://img.shields.io/badge/python-3.7%2C%203.8%2C%203.9%2C%203.10%2C%203.11-blue) +[![Build Status](https://github.com/AcademySoftwareFoundation/OpenTimelineIO/actions/workflows/python-package.yml/badge.svg)](https://github.com/AcademySoftwareFoundation/OpenTimelineIO/actions/workflows/python-package.yml) +[![codecov](https://codecov.io/gh/AcademySoftwareFoundation/OpenTimelineIO/branch/main/graph/badge.svg)](https://codecov.io/gh/AcademySoftwareFoundation/OpenTimelineIO) [![docs](https://readthedocs.org/projects/opentimelineio/badge/?version=latest)](https://opentimelineio.readthedocs.io/en/latest/index.html) +[![CII Best Practices](https://bestpractices.coreinfrastructure.org/projects/2288/badge)](https://bestpractices.coreinfrastructure.org/projects/2288) -Main web site: http://opentimeline.io/ +Links +----- -Documentation: https://opentimelineio.readthedocs.io/ - -GitHub: https://github.com/PixarAnimationStudios/OpenTimelineIO - -Discussion group: https://lists.aswf.io/g/otio-discussion - -Slack channel: https://academysoftwarefdn.slack.com/messages/CMQ9J4BQC -To join, create an account here first: https://slack.aswf.io/ +* Main web site: http://opentimeline.io/ +* Documentation: https://opentimelineio.readthedocs.io/ +* GitHub: https://github.com/AcademySoftwareFoundation/OpenTimelineIO +* [Discussion group](https://lists.aswf.io/g/otio-discussion) +* [Slack channel](https://academysoftwarefdn.slack.com/messages/CMQ9J4BQC) + * To join, create an account here first: https://slack.aswf.io/ +* [Presentations](https://github.com/AcademySoftwareFoundation/OpenTimelineIO/wiki/Presentations) PUBLIC BETA NOTICE ------------------ @@ -32,17 +33,19 @@ Overview -------- OpenTimelineIO is an interchange format and API for editorial cut information. -OTIO is not a container format for media, rather it contains information about -the order and length of cuts and references to external media. +OTIO contains information about the order and length of cuts and + references to external media. It is not however, a container format for media. -OTIO includes both a file format and an API for manipulating that format. It -also includes a plugin architecture for writing adapters to convert -from/to existing editorial timeline formats. It also implements a dependency- -less library for dealing strictly with time, `opentime`. +For integration with applications, the core OTIO library is implemented in C++ +and provides an in-memory data model, as well as library functions for +interpreting, manipulating, and serializing that data model. Within the core is +a dependency-less library for dealing strictly with time, `opentime`. -You can provide adapters for your video editing tool or pipeline as needed. -Each adapter allows for import/export between that proprietary tool and the -OpenTimelineIO format. +The project also supports an official python binding, which is intended to be +an idiomatic and ergonomic binding for python developers. The python binding +includes a plugin system which supports a number of different types of plugins, +most notably adapters, which can be used to read and write legacy formats into +the OTIO data model. Documentation -------------- @@ -51,24 +54,25 @@ Documentation, including quick start, architecture, use cases, API docs, and muc Supported VFX Platforms ----------------- The current release supports: -- VFX platform 2021, 2020, 2019, 2018 -- Python 2.7 - 3.10 +- VFX platform 2023, 2022, 2021, 2020 +- Python 3.7 - 3.10 For more information on our vfxplatform support policy: [Contribution Guidelines Documentation Page](https://opentimelineio.readthedocs.io/en/latest/tutorials/contributing.html) For more information on the vfxplatform: [VFX Platform Homepage](https://vfxplatform.com) -Adapters --------- +Adapter Plugins +--------------- -OpenTimelineIO supports, or plans to support, conversion adapters for many -existing file formats, such as Final Cut Pro XML, AAF, CMX 3600 EDL, etc. +To provide interoperability with other file formats or applications lacking a +native integration, the opentimelineio community has built a number of python +adapter plugins. This includes Final Cut Pro XML, AAF, CMX 3600 EDL, and more. -See: https://opentimelineio.readthedocs.io/en/latest/tutorials/adapters.html +For more information about this, including supported formats, see: https://opentimelineio.readthedocs.io/en/latest/tutorials/adapters.html Other Plugins ------------- -OTIO also supports several other kinds of plugins, for more information see: +The OTIO python bindings also support several other kinds of plugins, for more information see: * [Media Linkers](https://opentimelineio.readthedocs.io/en/latest/tutorials/write-a-media-linker.html) - Generate media references to local media according to your local conventions. * [HookScripts](https://opentimelineio.readthedocs.io/en/latest/tutorials/write-a-hookscript.html) - Scripts that can run at various points during OTIO execution (_ie_ before the media linker) @@ -77,7 +81,7 @@ OTIO also supports several other kinds of plugins, for more information see: Installing / Quick-Start ------------------------ -The python-wrapped version of OpenTimelineIO is publicly available via pypy. You can install OpenTimelineIO via: +The Python-wrapped version of OpenTimelineIO is publicly available [via PyPI](https://pypi.org/project/OpenTimelineIO/). You can install OpenTimelineIO via: `python -m pip install opentimelineio` @@ -87,18 +91,50 @@ For detailed installation instructions and notes on how to run the included view Example Usage ------------- +C++: + +```c++ +#include + +#include "opentimelineio/timeline.h" + +namespace otio = opentimelineio::OPENTIMELINEIO_VERSION; + +void +main() +{ + otio::ErrorStatus err; + otio::SerializableObject::Retainer tl( + dynamic_cast( + otio::Timeline::from_json_file("taco.otio", &err) + ) + ); + const std::vector> clips = ( + tl->find_clips() + ); + for (const auto& cl : clips) + { + otio::RationalTime dur = cl->duration(); + std::cout << "Name: " << cl->name() << " ["; + std::cout << dur.value() << "/" << dur.rate() << "]" << std::endl; + } +} ``` + +Python: + +```python import opentimelineio as otio timeline = otio.adapters.read_from_file("foo.aaf") -for clip in timeline.each_clip(): +for clip in timeline.find_clips(): print(clip.name, clip.duration()) ``` -There are more code examples here: https://github.com/PixarAnimationStudios/OpenTimelineIO/tree/main/examples +There are more code examples here: https://github.com/AcademySoftwareFoundation/OpenTimelineIO/tree/main/examples Also, looking through the unit tests is a great way to see what OTIO can do: -https://github.com/PixarAnimationStudios/OpenTimelineIO/tree/main/tests +https://github.com/AcademySoftwareFoundation/OpenTimelineIO/tree/main/tests OTIO includes a viewer program as well (see the quickstart section for instructions on installing it): @@ -111,7 +147,7 @@ If you want to contribute to the project, please see: https://opentimelineio.rea You can get the latest development version via: -`git clone git@github.com:PixarAnimationStudios/OpenTimelineIO.git --recursive ` +`git clone git@github.com:AcademySoftwareFoundation/OpenTimelineIO.git --recursive ` You can install development dependencies with `python -m pip install .[dev]` @@ -119,7 +155,7 @@ You can also install the PySide2 dependency with `python -m pip install .[view]` You may need to escape the `[` depending on your shell, `\[view\]` . -Currently the code base is written against python 2.7, 3.7, 3.8 and 3.9, +Currently the code base is written against python 3.7, 3.8, 3.9, 3.10 and 3.11, in keeping with the pep8 style. We ask that before developers submit pull request, they: @@ -160,6 +196,6 @@ Contact ------- For more information, please visit http://opentimeline.io/ -or https://github.com/PixarAnimationStudios/OpenTimelineIO +or https://github.com/AcademySoftwareFoundation/OpenTimelineIO or join our discussion forum: https://lists.aswf.io/g/otio-discussion diff --git a/contrib/README.md b/contrib/README.md new file mode 100644 index 000000000..1ac37c66f --- /dev/null +++ b/contrib/README.md @@ -0,0 +1,23 @@ +# OpenTimelineIO Contrib Area + +The contrib area was a place to host adapters and application plugins submitted +by the OpenTimelineIO community. Those contributions may have involved challenging external +dependencies, and may not have the same level of support as the core. + +The team is in the process of phasing out this area in favor of using individual +repos within the +[OpenTimelineIO GitHub Organization](https://github.com/OpenTimelineIO/). + +New contributions should be created in standalone repos, and those can later be +migrated to the OTIO GitHub organization after review. + +## Creating New Contrib Adapters and Plugins + +The best way to get started is to go to the +[otio-plugin-template repo](https://github.com/OpenTimelineIO/otio-plugin-template) +and click "Use this template". This will get you started with plugin boilerplate +and allow you to develop the adapter in your own GitHub account. + +Once you've developed your plugin, please contact the +[OpenTimelineIO team](https://github.com/AcademySoftwareFoundation/OpenTimelineIO) +to talk about next steps for the plugin. diff --git a/contrib/opentimelineio_contrib/__init__.py b/contrib/opentimelineio_contrib/__init__.py index cddf1086b..9d4c65d09 100644 --- a/contrib/opentimelineio_contrib/__init__.py +++ b/contrib/opentimelineio_contrib/__init__.py @@ -1,27 +1,7 @@ #!/usr/bin/env python # +# SPDX-License-Identifier: Apache-2.0 # Copyright Contributors to the OpenTimelineIO project -# -# Licensed under the Apache License, Version 2.0 (the "Apache License") -# with the following modification; you may not use this file except in -# compliance with the Apache License and the following modification to it: -# Section 6. Trademarks. is deleted and replaced with: -# -# 6. Trademarks. This License does not grant permission to use the trade -# names, trademarks, service marks, or product names of the Licensor -# and its affiliates, except as required to comply with Section 4(c) of -# the License and to reproduce the content of the NOTICE file. -# -# You may obtain a copy of the Apache License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the Apache License with the above modification is -# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -# KIND, either express or implied. See the Apache License for the specific -# language governing permissions and limitations under the Apache License. -# """Unsupported contrib code for OpenTimelineIO.""" diff --git a/contrib/opentimelineio_contrib/adapters/__init__.py b/contrib/opentimelineio_contrib/adapters/__init__.py index e69de29bb..686a8cb5f 100644 --- a/contrib/opentimelineio_contrib/adapters/__init__.py +++ b/contrib/opentimelineio_contrib/adapters/__init__.py @@ -0,0 +1,2 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright Contributors to the OpenTimelineIO project diff --git a/contrib/opentimelineio_contrib/adapters/aaf_adapter/__init__.py b/contrib/opentimelineio_contrib/adapters/aaf_adapter/__init__.py index e69de29bb..686a8cb5f 100644 --- a/contrib/opentimelineio_contrib/adapters/aaf_adapter/__init__.py +++ b/contrib/opentimelineio_contrib/adapters/aaf_adapter/__init__.py @@ -0,0 +1,2 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright Contributors to the OpenTimelineIO project diff --git a/contrib/opentimelineio_contrib/adapters/aaf_adapter/aaf_writer.py b/contrib/opentimelineio_contrib/adapters/aaf_adapter/aaf_writer.py index 2150f4bcf..63e6749e3 100644 --- a/contrib/opentimelineio_contrib/adapters/aaf_adapter/aaf_writer.py +++ b/contrib/opentimelineio_contrib/adapters/aaf_adapter/aaf_writer.py @@ -1,26 +1,5 @@ -# +# SPDX-License-Identifier: Apache-2.0 # Copyright Contributors to the OpenTimelineIO project -# -# Licensed under the Apache License, Version 2.0 (the "Apache License") -# with the following modification; you may not use this file except in -# compliance with the Apache License and the following modification to it: -# Section 6. Trademarks. is deleted and replaced with: -# -# 6. Trademarks. This License does not grant permission to use the trade -# names, trademarks, service marks, or product names of the Licensor -# and its affiliates, except as required to comply with Section 4(c) of -# the License and to reproduce the content of the NOTICE file. -# -# You may obtain a copy of the Apache License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the Apache License with the above modification is -# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -# KIND, either express or implied. See the Apache License for the specific -# language governing permissions and limitations under the Apache License. -# """AAF Adapter Transcriber @@ -83,7 +62,7 @@ class AAFValidationError(AAFAdapterError): pass -class AAFFileTranscriber(object): +class AAFFileTranscriber: """ AAFFileTranscriber @@ -158,7 +137,7 @@ def track_transcriber(self, otio_track): transcriber = AudioTrackTranscriber(self, otio_track) else: raise otio.exceptions.NotSupportedError( - "Unsupported track kind: {}".format(otio_track.kind)) + f"Unsupported track kind: {otio_track.kind}") return transcriber @@ -168,7 +147,7 @@ def validate_metadata(timeline): all_checks = [__check(timeline, "duration().rate")] edit_rate = __check(timeline, "duration().rate").value - for child in timeline.each_child(): + for child in timeline.find_children(): checks = [] if _is_considered_gap(child): checks = [ @@ -248,7 +227,7 @@ def _generate_empty_mobid(clip): clip_mob_ids = {} - for otio_clip in input_otio.each_clip(): + for otio_clip in input_otio.find_clips(): if _is_considered_gap(otio_clip): continue for strategy in strategies: @@ -257,7 +236,7 @@ def _generate_empty_mobid(clip): clip_mob_ids[otio_clip] = mob_id break else: - raise AAFAdapterError("Cannot find mob ID for clip {}".format(otio_clip)) + raise AAFAdapterError(f"Cannot find mob ID for clip {otio_clip}") return clip_mob_ids @@ -270,7 +249,7 @@ def _stackify_nested_groups(timeline): """ copied = copy.deepcopy(timeline) for track in copied.tracks: - for i, child in enumerate(track.each_child()): + for i, child in enumerate(track.find_children()): is_nested = isinstance(child, otio.schema.Track) is_parent_in_stack = isinstance(child.parent(), otio.schema.Stack) if is_nested and not is_parent_in_stack: @@ -281,7 +260,7 @@ def _stackify_nested_groups(timeline): return copied -class _TrackTranscriber(object): +class _TrackTranscriber: """ _TrackTranscriber is the base class for the conversion of a given otio track. @@ -303,7 +282,7 @@ def __init__(self, root_file_transcriber, otio_track): self.compositionmob = root_file_transcriber.compositionmob self.aaf_file = root_file_transcriber.aaf_file self.otio_track = otio_track - self.edit_rate = next(self.otio_track.each_child()).duration().rate + self.edit_rate = self.otio_track.find_children()[0].duration().rate self.timeline_mobslot, self.sequence = self._create_timeline_mobslot() self.timeline_mobslot.name = self.otio_track.name @@ -326,7 +305,7 @@ def transcribe(self, otio_child): return operation_group else: raise otio.exceptions.NotSupportedError( - "Unsupported otio child type: {}".format(type(otio_child))) + f"Unsupported otio child type: {type(otio_child)}") @property @abc.abstractmethod @@ -683,11 +662,11 @@ def aaf_sourceclip(self, otio_clip): length = int(otio_clip.duration().value) c1 = self.aaf_file.create.ControlPoint() c1["ControlPointSource"].value = 2 - c1["Time"].value = aaf2.rational.AAFRational("0/{}".format(length)) + c1["Time"].value = aaf2.rational.AAFRational(f"0/{length}") c1["Value"].value = 0 c2 = self.aaf_file.create.ControlPoint() c2["ControlPointSource"].value = 2 - c2["Time"].value = aaf2.rational.AAFRational("{}/{}".format(length - 1, length)) + c2["Time"].value = aaf2.rational.AAFRational(f"{length - 1}/{length}") c2["Value"].value = 0 varying_value = self.aaf_file.create.VaryingValue() varying_value.parameterdef = param_def @@ -696,7 +675,7 @@ def aaf_sourceclip(self, otio_clip): opgroup = self.timeline_mobslot.segment opgroup.parameters.append(varying_value) - return super(AudioTrackTranscriber, self).aaf_sourceclip(otio_clip) + return super().aaf_sourceclip(otio_clip) def _create_timeline_mobslot(self): """ @@ -759,7 +738,7 @@ def _transition_parameters(self): return [param_def_level], level -class __check(object): +class __check: """ __check is a private helper class that safely gets values given to check for existence and equality diff --git a/contrib/opentimelineio_contrib/adapters/advanced_authoring_format.py b/contrib/opentimelineio_contrib/adapters/advanced_authoring_format.py index 343ca398b..41feb2bbd 100644 --- a/contrib/opentimelineio_contrib/adapters/advanced_authoring_format.py +++ b/contrib/opentimelineio_contrib/adapters/advanced_authoring_format.py @@ -1,26 +1,5 @@ -# +# SPDX-License-Identifier: Apache-2.0 # Copyright Contributors to the OpenTimelineIO project -# -# Licensed under the Apache License, Version 2.0 (the "Apache License") -# with the following modification; you may not use this file except in -# compliance with the Apache License and the following modification to it: -# Section 6. Trademarks. is deleted and replaced with: -# -# 6. Trademarks. This License does not grant permission to use the trade -# names, trademarks, service marks, or product names of the Licensor -# and its affiliates, except as required to comply with Section 4(c) of -# the License and to reproduce the content of the NOTICE file. -# -# You may obtain a copy of the Apache License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the Apache License with the above modification is -# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -# KIND, either express or implied. See the Apache License for the specific -# language governing permissions and limitations under the Apache License. -# """OpenTimelineIO Advanced Authoring Format (AAF) Adapter @@ -33,18 +12,7 @@ import os import sys -try: - # Python 2 - text_type = unicode -except NameError: - # Python 3 - text_type = str - -try: - # Python 3.3+ - import collections.abc as collections_abc -except ImportError: - import collections as collections_abc +import collections import fractions import opentimelineio as otio @@ -87,7 +55,7 @@ class AAFAdapterError(otio.exceptions.OTIOError): def _get_parameter(item, parameter_name): - values = dict((value.name, value) for value in item.parameters.value) + values = {value.name: value for value in item.parameters.value} return values.get(parameter_name) @@ -120,9 +88,7 @@ def _get_class_name(item): def _transcribe_property(prop, owner=None): - # XXX: The unicode type doesn't exist in Python 3 (all strings are unicode) - # so we have to use type(u"") which works in both Python 2 and 3. - if isinstance(prop, (str, type(u""), numbers.Integral, float, dict)): + if isinstance(prop, (str, numbers.Integral, float, dict)): return prop elif isinstance(prop, set): return list(prop) @@ -392,7 +358,7 @@ def _extract_timecode_info(mob): def _add_child(parent, child, source): if child is None: if debug: - print("Adding null child? {}".format(source)) + print(f"Adding null child? {source}") elif isinstance(child, otio.schema.Marker): parent.markers.append(child) else: @@ -430,7 +396,7 @@ def _transcribe(item, parents, edit_rate, indent=0): # complex than OTIO. if isinstance(item, aaf2.content.ContentStorage): - msg = "Creating SerializableCollection for {}".format(_encoded_name(item)) + msg = f"Creating SerializableCollection for {_encoded_name(item)}" _transcribe_log(msg, indent) result = otio.schema.SerializableCollection() @@ -440,7 +406,7 @@ def _transcribe(item, parents, edit_rate, indent=0): _add_child(result, child, mob) elif isinstance(item, aaf2.mobs.Mob): - _transcribe_log("Creating Timeline for {}".format(_encoded_name(item)), indent) + _transcribe_log(f"Creating Timeline for {_encoded_name(item)}", indent) result = otio.schema.Timeline() for slot in item.slots: @@ -463,7 +429,7 @@ def _transcribe(item, parents, edit_rate, indent=0): _encoded_name(item), clipUsage ) else: - itemMsg = "Creating SourceClip for {}".format(_encoded_name(item)) + itemMsg = f"Creating SourceClip for {_encoded_name(item)}" _transcribe_log(itemMsg, indent) result = otio.schema.Clip() @@ -620,7 +586,7 @@ def _transcribe(item, parents, edit_rate, indent=0): result.out_offset = otio.opentime.RationalTime(out_offset, edit_rate) elif isinstance(item, aaf2.components.Filler): - _transcribe_log("Creating Gap for {}".format(_encoded_name(item)), indent) + _transcribe_log(f"Creating Gap for {_encoded_name(item)}", indent) result = otio.schema.Gap() length = item.length @@ -630,7 +596,7 @@ def _transcribe(item, parents, edit_rate, indent=0): ) elif isinstance(item, aaf2.components.NestedScope): - msg = "Creating Stack for NestedScope for {}".format(_encoded_name(item)) + msg = f"Creating Stack for NestedScope for {_encoded_name(item)}" _transcribe_log(msg, indent) # TODO: Is this the right class? result = otio.schema.Stack() @@ -640,7 +606,7 @@ def _transcribe(item, parents, edit_rate, indent=0): _add_child(result, child, slot) elif isinstance(item, aaf2.components.Sequence): - msg = "Creating Track for Sequence for {}".format(_encoded_name(item)) + msg = f"Creating Track for Sequence for {_encoded_name(item)}" _transcribe_log(msg, indent) result = otio.schema.Track() @@ -660,13 +626,13 @@ def _transcribe(item, parents, edit_rate, indent=0): _add_child(result, child, component) elif isinstance(item, aaf2.components.OperationGroup): - msg = "Creating operationGroup for {}".format(_encoded_name(item)) + msg = f"Creating operationGroup for {_encoded_name(item)}" _transcribe_log(msg, indent) result = _transcribe_operation_group(item, parents, metadata, edit_rate, indent + 2) elif isinstance(item, aaf2.mobslots.TimelineMobSlot): - msg = "Creating Track for TimelineMobSlot for {}".format(_encoded_name(item)) + msg = f"Creating Track for TimelineMobSlot for {_encoded_name(item)}" _transcribe_log(msg, indent) result = otio.schema.Track() @@ -675,7 +641,7 @@ def _transcribe(item, parents, edit_rate, indent=0): _add_child(result, child, item.segment) elif isinstance(item, aaf2.mobslots.MobSlot): - msg = "Creating Track for MobSlot for {}".format(_encoded_name(item)) + msg = f"Creating Track for MobSlot for {_encoded_name(item)}" _transcribe_log(msg, indent) result = otio.schema.Track() @@ -692,7 +658,7 @@ def _transcribe(item, parents, edit_rate, indent=0): pass elif isinstance(item, aaf2.components.ScopeReference): - msg = "Creating Gap for ScopedReference for {}".format(_encoded_name(item)) + msg = f"Creating Gap for ScopedReference for {_encoded_name(item)}" _transcribe_log(msg, indent) # TODO: is this like FILLER? @@ -708,7 +674,7 @@ def _transcribe(item, parents, edit_rate, indent=0): event_mobs = [p for p in parents if isinstance(p, aaf2.mobslots.EventMobSlot)] if event_mobs: _transcribe_log( - "Create marker for '{}'".format(_encoded_name(item)), indent + f"Create marker for '{_encoded_name(item)}'", indent ) result = otio.schema.Marker() @@ -752,7 +718,7 @@ def _transcribe(item, parents, edit_rate, indent=0): ) elif isinstance(item, aaf2.components.Selector): - msg = "Transcribe selector for {}".format(_encoded_name(item)) + msg = f"Transcribe selector for {_encoded_name(item)}" _transcribe_log(msg, indent) selected = item.getvalue('Selected') @@ -787,7 +753,7 @@ def _transcribe(item, parents, edit_rate, indent=0): # Perform a check here to make sure no potential Gap objects # are slipping through the cracks if isinstance(result, otio.schema.Gap): - err = "AAF Selector parsing error: {}".format(type(item)) + err = f"AAF Selector parsing error: {type(item)}" raise AAFAdapterError(err) # A Selector can have a set of alternates to handle multiple options for an @@ -845,7 +811,7 @@ def _transcribe(item, parents, edit_rate, indent=0): # elif isinstance(item, pyaaf.AxProperty): # self.properties['Value'] = str(item.GetValue()) - elif isinstance(item, collections_abc.Iterable): + elif isinstance(item, collections.abc.Iterable): msg = "Creating SerializableCollection for Iterable for {}".format( _encoded_name(item)) _transcribe_log(msg, indent) @@ -857,7 +823,7 @@ def _transcribe(item, parents, edit_rate, indent=0): # For everything else, we just ignore it. # To see what is being ignored, turn on the debug flag if debug: - print("SKIPPING: {}: {} -- {}".format(type(item), item, result)) + print(f"SKIPPING: {type(item)}: {item} -- {result}") # Did we get anything? If not, we're done if result is None: @@ -932,7 +898,7 @@ def _find_timecode_track_start(track): start = aaf_metadata["Segment"]["Start"] except KeyError as e: raise AAFAdapterError( - "Timecode missing '{}'".format(e) + f"Timecode missing '{e}'" ) if edit_rate.denominator == 1: @@ -1242,11 +1208,11 @@ def _attach_markers(collection): """ # iterate all timeline objects - for timeline in collection.each_child(descended_from_type=otio.schema.Timeline): + for timeline in collection.find_children(descended_from_type=otio.schema.Timeline): tracks_map = {} # build track mapping - for track in timeline.each_child(descended_from_type=otio.schema.Track): + for track in timeline.find_children(descended_from_type=otio.schema.Track): metadata = track.metadata.get("AAF", {}) slot_id = metadata.get("SlotID") track_number = metadata.get("PhysicalTrackNumber") @@ -1256,7 +1222,8 @@ def _attach_markers(collection): tracks_map[(int(slot_id), int(track_number))] = track # iterate all tracks for their markers and attach them to the matching item - for current_track in timeline.each_child(descended_from_type=otio.schema.Track): + for current_track in timeline.find_children( + descended_from_type=otio.schema.Track): for marker in list(current_track.markers): metadata = marker.metadata.get("AAF", {}) slot_id = metadata.get("AttachedSlotID") diff --git a/contrib/opentimelineio_contrib/adapters/ale.py b/contrib/opentimelineio_contrib/adapters/ale.py index 8b3c795f9..27a50011a 100644 --- a/contrib/opentimelineio_contrib/adapters/ale.py +++ b/contrib/opentimelineio_contrib/adapters/ale.py @@ -1,27 +1,5 @@ -# +# SPDX-License-Identifier: Apache-2.0 # Copyright Contributors to the OpenTimelineIO project -# -# Licensed under the Apache License, Version 2.0 (the "Apache License") -# with the following modification; you may not use this file except in -# compliance with the Apache License and the following modification to it: -# Section 6. Trademarks. is deleted and replaced with: -# -# 6. Trademarks. This License does not grant permission to use the trade -# names, trademarks, service marks, or product names of the Licensor -# and its affiliates, except as required to comply with Section 4(c) of -# the License and to reproduce the content of the NOTICE file. -# -# You may obtain a copy of the Apache License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the Apache License with the above modification is -# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -# KIND, either express or implied. See the Apache License for the specific -# language governing permissions and limitations under the Apache License. -# - __doc__ = """OpenTimelineIO Avid Log Exchange (ALE) Adapter""" @@ -82,7 +60,7 @@ def _parse_data_line(line, columns, fps, ale_name_column_key='Name'): try: start = otio.opentime.from_timecode(value, fps) except (ValueError, TypeError): - raise ALEParseError("Invalid Start timecode: {}".format(value)) + raise ALEParseError(f"Invalid Start timecode: {value}") duration = None end = None if metadata.get("Duration", "") != "": @@ -159,7 +137,7 @@ def _parse_data_line(line, columns, fps, ale_name_column_key='Name'): def _cdl_values_from_metadata(asc_sop_string): - if not isinstance(asc_sop_string, (type(''), type(u''))): + if not isinstance(asc_sop_string, str): return {} asc_sop_values = ASC_SOP_REGEX.findall(asc_sop_string) @@ -274,7 +252,7 @@ def nextline(lines): def write_to_string(input_otio, columns=None, fps=None, video_format=None): # Get all the clips we're going to export - clips = list(input_otio.each_clip()) + clips = list(input_otio.find_clips()) result = "" @@ -307,7 +285,7 @@ def write_to_string(input_otio, columns=None, fps=None, video_format=None): headers = list(header.items()) headers.sort() # make the output predictable for key, val in headers: - result += "{}\t{}\n".format(key, val) + result += f"{key}\t{val}\n" # If the caller passed in a list of columns, use that, otherwise # we need to discover the columns that should be output. diff --git a/contrib/opentimelineio_contrib/adapters/burnins.py b/contrib/opentimelineio_contrib/adapters/burnins.py index d6fd5399e..56212753e 100644 --- a/contrib/opentimelineio_contrib/adapters/burnins.py +++ b/contrib/opentimelineio_contrib/adapters/burnins.py @@ -1,26 +1,6 @@ -# +# SPDX-License-Identifier: Apache-2.0 # Copyright Contributors to the OpenTimelineIO project -# -# Licensed under the Apache License, Version 2.0 (the "Apache License") -# with the following modification; you may not use this file except in -# compliance with the Apache License and the following modification to it: -# Section 6. Trademarks. is deleted and replaced with: -# -# 6. Trademarks. This License does not grant permission to use the trade -# names, trademarks, service marks, or product names of the Licensor -# and its affiliates, except as required to comply with Section 4(c) of -# the License and to reproduce the content of the NOTICE file. -# -# You may obtain a copy of the Apache License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the Apache License with the above modification is -# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -# KIND, either express or implied. See the Apache License for the specific -# language governing permissions and limitations under the Apache License. -# + """FFMPEG Burnins Adapter""" import os import sys @@ -41,7 +21,7 @@ def build_burnins(input_otio): key = 'burnins' burnins = [] - for clip in input_otio.each_clip(): + for clip in input_otio.find_clips(): # per clip burnin data burnin_data = clip.media_reference.metadata.get(key) diff --git a/contrib/opentimelineio_contrib/adapters/contrib_adapters.plugin_manifest.json b/contrib/opentimelineio_contrib/adapters/contrib_adapters.plugin_manifest.json index cee294629..31fc45c9e 100644 --- a/contrib/opentimelineio_contrib/adapters/contrib_adapters.plugin_manifest.json +++ b/contrib/opentimelineio_contrib/adapters/contrib_adapters.plugin_manifest.json @@ -4,72 +4,50 @@ { "OTIO_SCHEMA": "Adapter.1", "name": "fcpx_xml", - "execution_scope": "in process", "filepath": "fcpx_xml.py", "suffixes": ["fcpxml"] }, { "OTIO_SCHEMA": "Adapter.1", "name": "hls_playlist", - "execution_scope": "in process", "filepath": "hls_playlist.py", "suffixes": ["m3u8"] }, - { - "OTIO_SCHEMA" : "Adapter.1", - "name" : "rv_session", - "execution_scope" : "in process", - "filepath" : "rv.py", - "suffixes" : ["rv"] - }, { "OTIO_SCHEMA" : "Adapter.1", "name" : "maya_sequencer", - "execution_scope" : "in process", "filepath" : "maya_sequencer.py", "suffixes" : ["ma","mb"] }, { "OTIO_SCHEMA" : "Adapter.1", "name" : "ale", - "execution_scope" : "in process", "filepath" : "ale.py", "suffixes" : ["ale"] }, { "OTIO_SCHEMA" : "Adapter.1", "name" : "burnins", - "execution_scope" : "in process", "filepath" : "burnins.py", "suffixes" : [] }, { "OTIO_SCHEMA" : "Adapter.1", "name" : "AAF", - "execution_scope" : "in process", "filepath" : "advanced_authoring_format.py", "suffixes" : ["aaf"] }, { "OTIO_SCHEMA": "Adapter.1", "name": "xges", - "execution_scope": "in process", "filepath": "xges.py", "suffixes": ["xges"] - }, - { - "OTIO_SCHEMA": "Adapter.1", - "name": "kdenlive", - "execution_scope": "in process", - "filepath": "kdenlive.py", - "suffixes": ["kdenlive"] } ], "schemadefs" : [ { "OTIO_SCHEMA" : "SchemaDef.1", "name" : "xges", - "execution_scope" : "in process", "filepath" : "xges.py" } ] diff --git a/contrib/opentimelineio_contrib/adapters/extern_maya_sequencer.py b/contrib/opentimelineio_contrib/adapters/extern_maya_sequencer.py index f320a4d63..d5419c1c4 100644 --- a/contrib/opentimelineio_contrib/adapters/extern_maya_sequencer.py +++ b/contrib/opentimelineio_contrib/adapters/extern_maya_sequencer.py @@ -1,35 +1,10 @@ -# +# SPDX-License-Identifier: Apache-2.0 # Copyright Contributors to the OpenTimelineIO project -# -# Licensed under the Apache License, Version 2.0 (the "Apache License") -# with the following modification; you may not use this file except in -# compliance with the Apache License and the following modification to it: -# Section 6. Trademarks. is deleted and replaced with: -# -# 6. Trademarks. This License does not grant permission to use the trade -# names, trademarks, service marks, or product names of the Licensor -# and its affiliates, except as required to comply with Section 4(c) of -# the License and to reproduce the content of the NOTICE file. -# -# You may obtain a copy of the Apache License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the Apache License with the above modification is -# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -# KIND, either express or implied. See the Apache License for the specific -# language governing permissions and limitations under the Apache License. -# import os import sys -# deal with renaming of default library from python 2 / 3 -try: - import urlparse as urllib_parse -except ImportError: - import urllib.parse as urllib_parse +import urllib.parse # import maya and handle standalone mode from maya import cmds @@ -58,7 +33,7 @@ def _url_to_path(url): if url is None: return None - return urllib_parse.urlparse(url).path + return urllib.parse.urlparse(url).path def _video_url_for_shot(shot): diff --git a/contrib/opentimelineio_contrib/adapters/extern_rv.py b/contrib/opentimelineio_contrib/adapters/extern_rv.py deleted file mode 100755 index 57b64bb39..000000000 --- a/contrib/opentimelineio_contrib/adapters/extern_rv.py +++ /dev/null @@ -1,121 +0,0 @@ -# -# Copyright Contributors to the OpenTimelineIO project -# -# Licensed under the Apache License, Version 2.0 (the "Apache License") -# with the following modification; you may not use this file except in -# compliance with the Apache License and the following modification to it: -# Section 6. Trademarks. is deleted and replaced with: -# -# 6. Trademarks. This License does not grant permission to use the trade -# names, trademarks, service marks, or product names of the Licensor -# and its affiliates, except as required to comply with Section 4(c) of -# the License and to reproduce the content of the NOTICE file. -# -# You may obtain a copy of the Apache License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the Apache License with the above modification is -# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -# KIND, either express or implied. See the Apache License for the specific -# language governing permissions and limitations under the Apache License. -# - -"""RV External Adapter component. - -Because the rv adapter requires being run from within the RV py-interp to take -advantage of modules inside of RV, this script gets shelled out to from the -RV OTIO adapter. - -Requires that you set the environment variables: - OTIO_RV_PYTHON_LIB - should point at the parent directory of rvSession - OTIO_RV_PYTHON_BIN - should point at py-interp from within rv -""" - -# python -import sys -import os -import json - -# rv import -sys.path += [os.path.join(os.environ["OTIO_RV_PYTHON_LIB"], "rvSession")] -import rvSession # noqa - - -_RV_TYPE_MAP = { - "rvSession.gto.FLOAT": rvSession.gto.FLOAT, - "rvSession.gto.STRING": rvSession.gto.STRING, -} - - -# because json.loads returns a unicode type -_UNICODE_TYPE = type(u"") - - -def main(): - """ entry point, should be called from the rv adapter in otio """ - - session_file = rvSession.Session() - - output_fname = sys.argv[1] - - simplified_data = json.loads(sys.stdin.read()) - if sys.version_info.major <= 2: - simplified_data = _remove_unicode(simplified_data) - - result = execute_rv_commands(simplified_data, session_file) - - session_file.setViewNode(result) - session_file.write(output_fname) - - -def execute_rv_commands(simplified_data, to_session): - rv_nodes = [] - for node in simplified_data["nodes"]: - new_node = to_session.newNode(str(node["kind"]), str(node["name"])) - rv_node_index = len(rv_nodes) - - # make sure that node order lines up - assert(rv_node_index == node["node_index"]) - - rv_nodes.append(new_node) - node["rv_node"] = new_node - - for prop in node["properties"]: - args = prop - # the fourth argument is the type - args[4] = _RV_TYPE_MAP[args[4]] - - new_node.setProperty(*args) - - for (fn, args) in node["commands"]: - getattr(new_node, fn)(args) - - # inputs done as a second pass now that all nodes are created - for node in simplified_data["nodes"]: - for input in node["inputs"]: - node["rv_node"].addInput(rv_nodes[input]) - - # return the first node created. - return rv_nodes[0] - - -def _remove_unicode(blob): - if _UNICODE_TYPE == type(blob): - return blob.encode('utf-8') - - if isinstance(blob, dict): - result = {} - for key, val in blob.items(): - result[_remove_unicode(key)] = _remove_unicode(val) - return result - - if isinstance(blob, list): - return [_remove_unicode(i) for i in blob] - - return blob - - -if __name__ == "__main__": - main() diff --git a/contrib/opentimelineio_contrib/adapters/fcpx_xml.py b/contrib/opentimelineio_contrib/adapters/fcpx_xml.py index 4b757d188..47f71e0da 100644 --- a/contrib/opentimelineio_contrib/adapters/fcpx_xml.py +++ b/contrib/opentimelineio_contrib/adapters/fcpx_xml.py @@ -1,26 +1,5 @@ -# +# SPDX-License-Identifier: Apache-2.0 # Copyright Contributors to the OpenTimelineIO project -# -# Licensed under the Apache License, Version 2.0 (the "Apache License") -# with the following modification; you may not use this file except in -# compliance with the Apache License and the following modification to it: -# Section 6. Trademarks. is deleted and replaced with: -# -# 6. Trademarks. This License does not grant permission to use the trade -# names, trademarks, service marks, or product names of the Licensor -# and its affiliates, except as required to comply with Section 4(c) of -# the License and to reproduce the content of the NOTICE file. -# -# You may obtain a copy of the Apache License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the Apache License with the above modification is -# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -# KIND, either express or implied. See the Apache License for the specific -# language governing permissions and limitations under the Apache License. -# """OpenTimelineIO Final Cut Pro X XML Adapter. """ import os @@ -29,11 +8,7 @@ from xml.dom import minidom from fractions import Fraction from datetime import date - -try: - from urllib import unquote -except ImportError: - from urllib.parse import unquote +from urllib.parse import unquote import opentimelineio as otio @@ -98,7 +73,7 @@ def format_name(frame_rate, path): if frame_size.endswith("1280"): frame_size = "720" - return "FFVideoFormat{}p{}".format(frame_size, frame_rate) + return f"FFVideoFormat{frame_size}p{frame_rate}" def to_rational_time(rational_number, fps): @@ -144,11 +119,11 @@ def from_rational_time(rational_time): float(rational_time.value) / float(rational_time.rate) ).limit_denominator() if str(result.denominator) == "1": - return "{}s".format(result.numerator) - return "{}/{}s".format(result.numerator, result.denominator) + return f"{result.numerator}s" + return f"{result.numerator}/{result.denominator}s" -class FcpxOtio(object): +class FcpxOtio: """ This object is responsible for knowing how to convert an otio into an FCP X XML @@ -165,7 +140,7 @@ def __init__(self, otio_timeline): self.timelines = [self.otio_timeline] else: self.timelines = list( - self.otio_timeline.each_child( + self.otio_timeline.find_children( descended_from_type=otio.schema.Timeline ) ) @@ -270,7 +245,7 @@ def _stack_to_sequence(self, stack, compound_clip=False): return sequence_element def _track_for_spine(self, track, lane_id, spine, compound): - for child in self._lanable_items(track.each_child()): + for child in self._lanable_items(track.find_children()): if self._item_in_compound_clip(child) and not compound: continue child_element = self._element_for_item( @@ -363,7 +338,7 @@ def _frame_rate_from_element(self, element, default_format_id): format_id = asset.get("format") format_element = self.resource_element.find( - "./format[@id='{}']".format(format_id) + f"./format[@id='{format_id}']" ) total, rate = format_element.get("frameDuration").split("/") rate = rate.replace("s", "") @@ -650,18 +625,18 @@ def _add_compound_clip(self, item): return media_element def _stacks(self): - return self.otio_timeline.each_child( + return self.otio_timeline.find_children( descended_from_type=otio.schema.Stack ) def _clips(self): - return self.otio_timeline.each_child( + return self.otio_timeline.find_children( descended_from_type=otio.schema.Clip ) def _resource_id_generator(self): self.resource_count += 1 - return "r{}".format(self.resource_count) + return f"r{self.resource_count}" def _event_name(self): if self.otio_timeline.name: @@ -669,26 +644,26 @@ def _event_name(self): return date.strftime(date.today(), "%m-%e-%y") def _asset_by_path(self, path): - return self.resource_element.find("./asset[@src='{}']".format(path)) + return self.resource_element.find(f"./asset[@src='{path}']") def _asset_by_id(self, asset_id): - return self.resource_element.find("./asset[@id='{}']".format(asset_id)) + return self.resource_element.find(f"./asset[@id='{asset_id}']") def _media_by_name(self, name): - return self.resource_element.find("./media[@name='{}']".format(name)) + return self.resource_element.find(f"./media[@name='{name}']") def _media_by_id(self, media_id): - return self.resource_element.find("./media[@id='{}']".format(media_id)) + return self.resource_element.find(f"./media[@id='{media_id}']") def _format_by_frame_rate(self, frame_rate): frame_duration = self._framerate_to_frame_duration(frame_rate) return self.resource_element.find( - "./format[@frameDuration='{}']".format(frame_duration) + f"./format[@frameDuration='{frame_duration}']" ) def _asset_clip_by_name(self, name): return self.event_resource.find( - "./asset-clip[@name='{}']".format(name) + f"./asset-clip[@name='{name}']" ) # -------------------- @@ -707,20 +682,20 @@ def _target_url_from_clip(clip): if (clip.media_reference and not clip.media_reference.is_missing_reference): return clip.media_reference.target_url - return "file:///tmp/{}".format(clip.name) + return f"file:///tmp/{clip.name}" @staticmethod def _calculate_rational_number(duration, rate): if int(duration) == 0: return "0s" result = Fraction(float(duration) / float(rate)).limit_denominator() - return "{}/{}s".format(result.numerator, result.denominator) + return f"{result.numerator}/{result.denominator}s" @staticmethod def _compound_clip_name(compound_clip, resource_id): if compound_clip.name: return compound_clip.name - return "compound_clip_{}".format(resource_id) + return f"compound_clip_{resource_id}" @staticmethod def _item_in_compound_clip(item): @@ -773,7 +748,7 @@ def _create_note_element(note): return note_element -class FcpxXml(object): +class FcpxXml: """ This object is responsible for knowing how to convert an FCP X XML otio into an otio timeline @@ -1115,24 +1090,24 @@ def _time_range(self, element, format_id): def _asset_by_id(self, asset_id): return self.fcpx_xml.find( - "./resources/asset[@id='{}']".format(asset_id) + f"./resources/asset[@id='{asset_id}']" ) def _assetclip_by_ref(self, asset_id): event = self.fcpx_xml.find("./event") if event is None: - return self.fcpx_xml.find("./asset-clip[@ref='{}']".format(asset_id)) + return self.fcpx_xml.find(f"./asset-clip[@ref='{asset_id}']") else: - return event.find("./asset-clip[@ref='{}']".format(asset_id)) + return event.find(f"./asset-clip[@ref='{asset_id}']") def _format_by_id(self, format_id): return self.fcpx_xml.find( - "./resources/format[@id='{}']".format(format_id) + f"./resources/format[@id='{format_id}']" ) def _compound_clip_by_id(self, compound_id): return self.fcpx_xml.find( - "./resources/media[@id='{}']".format(compound_id) + f"./resources/media[@id='{compound_id}']" ) # -------------------- diff --git a/contrib/opentimelineio_contrib/adapters/ffmpeg_burnins.py b/contrib/opentimelineio_contrib/adapters/ffmpeg_burnins.py index 4f8174f3f..198621567 100644 --- a/contrib/opentimelineio_contrib/adapters/ffmpeg_burnins.py +++ b/contrib/opentimelineio_contrib/adapters/ffmpeg_burnins.py @@ -1,26 +1,6 @@ -# +# SPDX-License-Identifier: Apache-2.0 # Copyright Contributors to the OpenTimelineIO project -# -# Licensed under the Apache License, Version 2.0 (the "Apache License") -# with the following modification; you may not use this file except in -# compliance with the Apache License and the following modification to it: -# Section 6. Trademarks. is deleted and replaced with: -# -# 6. Trademarks. This License does not grant permission to use the trade -# names, trademarks, service marks, or product names of the Licensor -# and its affiliates, except as required to comply with Section 4(c) of -# the License and to reproduce the content of the NOTICE file. -# -# You may obtain a copy of the Apache License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the Apache License with the above modification is -# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -# KIND, either express or implied. See the Apache License for the specific -# language governing permissions and limitations under the Apache License. -# + """ This module provides an interface to allow users to easily build out an FFMPEG command with all the correct filters @@ -121,15 +101,15 @@ class Options(dict): } def __init__(self, **kwargs): - super(Options, self).__init__() + super().__init__() params = self._params.copy() params.update(kwargs) - super(Options, self).update(**params) + super().update(**params) def __setitem__(self, key, value): if key not in self._params: raise KeyError("Not a valid option key '%s'" % key) - super(Options, self).update({key: value}) + super().update({key: value}) class FrameNumberOptions(Options): @@ -152,7 +132,7 @@ def __init__(self, **kwargs): 'frame_offset': 0, 'expression': None }) - super(FrameNumberOptions, self).__init__(**kwargs) + super().__init__(**kwargs) class TextOptions(Options): @@ -190,10 +170,10 @@ def __init__(self, **kwargs): 'frame_offset': 0, 'fps': 24 }) - super(TimeCodeOptions, self).__init__(**kwargs) + super().__init__(**kwargs) -class Burnins(object): +class Burnins: """ Class that provides convenience API for building filter flags for the FFMPEG command. @@ -419,7 +399,7 @@ def _drawtext(align, resolution, text, options): def _frames_to_timecode(frames, framerate): - return '{0:02d}:{1:02d}:{2:02d}:{3:02d}'.format( + return '{:02d}:{:02d}:{:02d}:{:02d}'.format( int(frames / (3600 * framerate)), int(frames / (60 * framerate) % 60), int(frames / framerate % 60), diff --git a/contrib/opentimelineio_contrib/adapters/hls_playlist.py b/contrib/opentimelineio_contrib/adapters/hls_playlist.py index a702f9195..6f55b1a87 100644 --- a/contrib/opentimelineio_contrib/adapters/hls_playlist.py +++ b/contrib/opentimelineio_contrib/adapters/hls_playlist.py @@ -1,26 +1,5 @@ -# +# SPDX-License-Identifier: Apache-2.0 # Copyright Contributors to the OpenTimelineIO project -# -# Licensed under the Apache License, Version 2.0 (the "Apache License") -# with the following modification; you may not use this file except in -# compliance with the Apache License and the following modification to it: -# Section 6. Trademarks. is deleted and replaced with: -# -# 6. Trademarks. This License does not grant permission to use the trade -# names, trademarks, service marks, or product names of the Licensor -# and its affiliates, except as required to comply with Section 4(c) of -# the License and to reproduce the content of the NOTICE file. -# -# You may obtain a copy of the Apache License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the Apache License with the above modification is -# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -# KIND, either express or implied. See the Apache License for the specific -# language governing permissions and limitations under the Apache License. -# """HLS Playlist OpenTimelineIO adapter @@ -129,15 +108,6 @@ # TODO: make sure all strings get sanitized through encoding and decoding PLAYLIST_STRING_ENCODING = "utf-8" -# Enable isinstance(my_instance, basestring) tests in Python 3 -# This can be phased out when Python 2 support is dropped. Replace tests with: -# isinstance(my_instance, str) - -try: - basestring -except NameError: - basestring = str - """ Matches a single key/value pair from an HLS Attribute List. See section 4.2 of draft-pantos-http-live-streaming for more detail. @@ -252,12 +222,12 @@ def __str__(self): out_value = '' if isinstance(v, AttributeListEnum): out_value = v - elif isinstance(v, basestring): - out_value = '"{}"'.format(v) + elif isinstance(v, str): + out_value = f'"{v}"' else: out_value = str(v) - attr_list_entries.append('{}={}'.format(k, out_value)) + attr_list_entries.append(f'{k}={out_value}') return ','.join(attr_list_entries) @@ -302,7 +272,7 @@ def from_string(cls, attrlist_string): BYTE_COUNT_KEY = 'byte_count' -class Byterange(object): +class Byterange: """Offers interpretation of HLS byte ranges in various forms.""" count = None @@ -341,7 +311,7 @@ def __str__(self): out_str = str(self.count) if self.offset is not None: - out_str += '@{}'.format(str(self.offset)) + out_str += f'@{str(self.offset)}' return out_str @@ -419,17 +389,17 @@ def from_dict(cls, info_dict): Basic tags appear in both media and master playlists. See section 4.3.1 of draft-pantos-http-live-streaming for more detail. """ -BASIC_TAGS = set([ +BASIC_TAGS = { "EXTM3U", "EXT-X-VERSION" -]) +} """ Media segment tags apply to either the following media or all subsequent segments. They MUST NOT appear in master playlists. See section 4.3.2 of draft-pantos-http-live-streaming for more detail. """ -MEDIA_SEGMENT_TAGS = set([ +MEDIA_SEGMENT_TAGS = { 'EXTINF', 'EXT-X-BYTERANGE', 'EXT-X-DISCONTINUITY', @@ -437,40 +407,40 @@ def from_dict(cls, info_dict): 'EXT-X-MAP', 'EXT-X-PROGRAM-DATE-TIME', 'EXT-X-DATERANGE' -]) +} """ The subset of above tags that apply to every segment following them """ -MEDIA_SEGMENT_SUBSEQUENT_TAGS = set([ +MEDIA_SEGMENT_SUBSEQUENT_TAGS = { 'EXT-X-KEY', 'EXT-X-MAP', -]) +} """ Media Playlist tags must only occur once per playlist, and must not appear in Master Playlists. See section 4.3.3 of draft-pantos-http-live-streaming for more detail. """ -MEDIA_PLAYLIST_TAGS = set([ +MEDIA_PLAYLIST_TAGS = { 'EXT-X-TARGETDURATION', 'EXT-X-MEDIA-SEQUENCE', 'EXT-X-DISCONTINUITY-SEQUENCE', 'EXT-X-ENDLIST', 'EXT-X-PLAYLIST-TYPE', 'EXT-X-I-FRAMES-ONLY' -]) +} """ Master playlist tags declare global parameters for the presentation. They must not appear in media playlists. See section 4.3.4 of draft-pantos-http-live-streaming for more detail. """ -MASTER_PLAYLIST_TAGS = set([ +MASTER_PLAYLIST_TAGS = { 'EXT-X-MEDIA', 'EXT-X-STREAM-INF', 'EXT-X-I-FRAME-STREAM-INF', 'EXT-X-SESSION-DATA', 'EXT-X-SESSION-KEY', -]) +} """ Media or Master Playlist tags can appear in either media or master playlists. @@ -479,10 +449,10 @@ def from_dict(cls, info_dict): in both, their values MUST agree. These values MUST NOT appear more than once in a playlist. """ -MEDIA_OR_MASTER_TAGS = set([ +MEDIA_OR_MASTER_TAGS = { "EXT-X-INDEPENDENT-SEGMENTS", "EXT-X-START" -]) +} """ Some special tags used by the parser. @@ -496,12 +466,12 @@ def from_dict(cls, info_dict): attribute list entries to omit from EXT-I-FRAME-STREAM-INF tags See section 4.3.4.3 of draft-pantos-http-live-streaming for more detail. """ -I_FRAME_OMIT_ATTRS = set([ +I_FRAME_OMIT_ATTRS = { 'FRAME-RATE', 'AUDIO', 'SUBTITLES', 'CLOSED-CAPTIONS' -]) +} """ enum for kinds of playlist entries """ EntryType = type('EntryType', (), { @@ -524,12 +494,12 @@ def from_dict(cls, info_dict): } """ mapping from otio ``TrackKind`` to HLS track type """ -OTIO_TRACK_KIND_TO_HLS_TYPE = dict(( - (v, k) for k, v in HLS_TRACK_TYPE_TO_OTIO_KIND.items() -)) +OTIO_TRACK_KIND_TO_HLS_TYPE = { + v: k for k, v in HLS_TRACK_TYPE_TO_OTIO_KIND.items() +} -class HLSPlaylistEntry(object): +class HLSPlaylistEntry: """An entry in an HLS playlist. Entries can be a tag, a comment, or a URI. All HLS playlists are parsed @@ -579,9 +549,9 @@ def __repr__(self): repr(self.tag_value) ) elif self.type == EntryType.comment: - base_str += ', comment={}'.format(repr(self.comment_string)) + base_str += f', comment={repr(self.comment_string)}' elif self.type == EntryType.URI: - base_str += ', URI={}'.format(repr(self.uri)) + base_str += f', URI={repr(self.uri)}' return base_str + ')' @@ -592,7 +562,7 @@ def __str__(self): :return: (:class:`str`) HLS playlist entry string. """ if self.type == EntryType.comment and self.comment_string: - return "# {}".format(self.comment_string) + return f"# {self.comment_string}" elif self.type == EntryType.comment: # empty comments are blank lines return "" @@ -601,9 +571,9 @@ def __str__(self): elif self.type == EntryType.tag: out_tag_name = self.tag_name if self.tag_value is not None: - return '#{}:{}'.format(out_tag_name, self.tag_value) + return f'#{out_tag_name}:{self.tag_value}' else: - return '#{}'.format(out_tag_name) + return f'#{out_tag_name}' @classmethod def tag_entry(cls, name, value=None): @@ -727,7 +697,7 @@ def parsed_tag_value(self, playlist_version=None): return group_dict -class HLSPlaylistParser(object): +class HLSPlaylistParser: """Bootstraps HLS parsing and hands the playlist string off to the appropriate parser for the type """ @@ -799,7 +769,7 @@ def _parse_playlist(self, edl_string): self.timeline.tracks.append(parser.track) -class MediaPlaylistParser(object): +class MediaPlaylistParser: """Parses an HLS Media playlist returning a SEQUENCE""" def __init__(self, playlist_entries, playlist_version=None): @@ -1011,7 +981,7 @@ def entries_for_segment( # add the EXTINF name = segment_name if segment_name is not None else '' - tag_value = '{0:.5f},{1}'.format( + tag_value = '{:.5f},{}'.format( otio.opentime.to_seconds(segment_duration), name ) @@ -1081,7 +1051,7 @@ def master_playlist_to_string(master_timeline): # start with a version number of 1, as features are encountered, we will # update the version accordingly - version_requirements = set([1]) + version_requirements = {1} # TODO: detect rather than forcing version 6 version_requirements.add(6) @@ -1095,7 +1065,7 @@ def master_playlist_to_string(master_timeline): hls_md_rejectlist = ['master_playlist'] for key in hls_md_rejectlist: try: - del(header_tags[key]) + del header_tags[key] except KeyError: pass @@ -1124,10 +1094,10 @@ def master_playlist_to_string(master_timeline): group_id = streaming_metadata['group_id'] except KeyError: sub_id = hls_type_count.setdefault(hls_type, 1) - group_id = '{}{}'.format(hls_type, sub_id) + group_id = f'{hls_type}{sub_id}' hls_type_count[hls_type] += 1 - media_playlist_default_uri = "{}.m3u8".format(track.name) + media_playlist_default_uri = f"{track.name}.m3u8" try: track_uri = track.metadata[FORMAT_METADATA_KEY].get( 'uri', @@ -1179,7 +1149,7 @@ def master_playlist_to_string(master_timeline): # Remove entries to not be included for I-Frame streams for attr in I_FRAME_OMIT_ATTRS: try: - del(attribute_list[attr]) + del attribute_list[attr] except KeyError: pass @@ -1204,7 +1174,7 @@ def master_playlist_to_string(master_timeline): al = stream_inf_attr_list_for_track(track) # Create the uri - media_playlist_default_uri = "{}.m3u8".format(track.name) + media_playlist_default_uri = f"{track.name}.m3u8" try: track_uri = track.metadata[FORMAT_METADATA_KEY].get( 'uri', media_playlist_default_uri @@ -1234,7 +1204,7 @@ def master_playlist_to_string(master_timeline): ) combo_al = copy.copy(al) - combo_al['CODECS'] += ',{}'.format(aud_codec) + combo_al['CODECS'] += f',{aud_codec}' combo_al['AUDIO'] = aud_group combo_al['BANDWIDTH'] += aud_bandwidth @@ -1279,7 +1249,7 @@ def master_playlist_to_string(master_timeline): out_entries += playlist_entries playlist_string = '\n'.join( - (str(entry) for entry in out_entries) + str(entry) for entry in out_entries ) return playlist_string @@ -1308,7 +1278,7 @@ def __init__( # Whenever an entry is added that has a minimum version requirement, # we add that version to this set. The max value from this set is the # playlist's version requirement - self._versions_used = set([1]) + self._versions_used = {1} # TODO: detect rather than forcing version 7 self._versions_used.add(7) @@ -1337,7 +1307,7 @@ def _copy_HLS_metadata(self, media_track): # Remove the version tag from the track metadata, we'll compute # based on what we write out - del(self._playlist_tags[PLAYLIST_VERSION_TAG]) + del self._playlist_tags[PLAYLIST_VERSION_TAG] except KeyError: pass @@ -1346,7 +1316,7 @@ def _copy_HLS_metadata(self, media_track): # playlist URIs for key in ('uri', 'iframe_uri'): try: - del(self._playlist_tags[key]) + del self._playlist_tags[key] except KeyError: pass @@ -1455,7 +1425,7 @@ def _add_entries_for_segment_from_fragments( omit_hls_keys = omit_hls_keys or [] for key in omit_hls_keys: try: - del(segment_tags[key]) + del segment_tags[key] except KeyError: pass @@ -1483,7 +1453,7 @@ def _add_entries_for_segment_from_fragments( omit_hls_keys = omit_hls_keys or [] for key in omit_hls_keys: try: - del(segment_tags[key]) + del segment_tags[key] except KeyError: pass @@ -1736,7 +1706,7 @@ def playlist_string(self): """Returns the string representation of the playlist entries""" return '\n'.join( - (str(entry) for entry in self._playlist_entries) + str(entry) for entry in self._playlist_entries ) # Public interface diff --git a/contrib/opentimelineio_contrib/adapters/kdenlive.py b/contrib/opentimelineio_contrib/adapters/kdenlive.py deleted file mode 100644 index 55fa54bf0..000000000 --- a/contrib/opentimelineio_contrib/adapters/kdenlive.py +++ /dev/null @@ -1,596 +0,0 @@ -# -# Copyright Contributors to the OpenTimelineIO project -# -# Licensed under the Apache License, Version 2.0 (the "Apache License") -# with the following modification; you may not use this file except in -# compliance with the Apache License and the following modification to it: -# Section 6. Trademarks. is deleted and replaced with: -# -# 6. Trademarks. This License does not grant permission to use the trade -# names, trademarks, service marks, or product names of the Licensor -# and its affiliates, except as required to comply with Section 4(c) of -# the License and to reproduce the content of the NOTICE file. -# -# You may obtain a copy of the Apache License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the Apache License with the above modification is -# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -# KIND, either express or implied. See the Apache License for the specific -# language governing permissions and limitations under the Apache License. -# - -"""Kdenlive (MLT XML) Adapter.""" -import re -import os -import sys -from xml.etree import ElementTree as ET -from xml.dom import minidom -import opentimelineio as otio -try: - from urllib.parse import urlparse, unquote -except ImportError: - # Python 2 - from urlparse import urlparse - from urllib import unquote - - -def read_property(element, name): - """Decode an MLT item property - which value is contained in a "property" XML element - with matching "name" attribute""" - return element.findtext("property[@name='{}']".format(name), '') - - -def time(clock, rate): - """Decode an MLT time - which is either a frame count or a timecode string - after format hours:minutes:seconds.floatpart""" - hms = [float(x) for x in clock.replace(',', '.').split(':')] - if len(hms) > 1: - smh = list(reversed(hms)) - hours = smh[2] if len(hms) > 2 else 0 - mins = smh[1] - secs = smh[0] - # unpick the same rounding/flooring from the clock function - # (N.B.: code from the clock function mimicks what - # I've seen from the mlt source code. - # It's technically wrong. Or at least, I believe - # it's written assuming an integer frame rate) - f = ( - round(secs * rate) - + int(mins * 60 * rate) - + int(hours * 3600 * rate) - ) - else: - f = hms[0] - return otio.opentime.RationalTime(f, rate) - - -def read_keyframes(kfstring, rate): - """Decode MLT keyframes - which are in a semicolon (;) separated list of time/value pair - separated by = (linear interp) or ~= (spline) or |= (step) - becomes a dict with RationalTime keys""" - return dict((str(time(t, rate).value), v) - for (t, v) in re.findall('([^|~=;]*)[|~]?=([^;]*)', kfstring)) - - -def read_from_string(input_str): - """Read a Kdenlive project (MLT XML) - Kdenlive uses a given MLT project layout, similar to Shotcut, - combining a "main_bin" playlist to organize source media, - and a "global_feed" tractor for timeline. - (in Kdenlive 19.x, timeline tracks include virtual sub-track, unused for now)""" - mlt, byid = ET.XMLID(input_str) - profile = mlt.find('profile') - rate = (float(profile.get('frame_rate_num')) - / float(profile.get('frame_rate_den', 1))) - timeline = otio.schema.Timeline( - name=mlt.get('name', 'Kdenlive imported timeline')) - - maintractor = mlt.find("tractor[@global_feed='1']") - # global_feed is no longer set in newer kdenlive versions - if maintractor is None: - alltractors = mlt.findall("tractor") - # the last tractor is the main tractor - maintractor = alltractors[-1] - # check all other tractors are used as tracks - for tractor in alltractors[:-1]: - if maintractor.find("track[@producer='%s']" % tractor.attrib['id']) is None: - raise RuntimeError("Can't find main tractor") - - for maintrack in maintractor.findall('track'): - if maintrack.get('producer') == 'black_track': - continue - subtractor = byid[maintrack.get('producer')] - track = otio.schema.Track( - name=read_property(subtractor, 'kdenlive:track_name')) - if bool(read_property(subtractor, 'kdenlive:audio_track')): - track.kind = otio.schema.TrackKind.Audio - else: - track.kind = otio.schema.TrackKind.Video - for subtrack in subtractor.findall('track'): - playlist = byid[subtrack.get('producer')] - for item in playlist.iter(): - if item.tag == 'blank': - gap = otio.schema.Gap( - duration=time(item.get('length'), rate)) - track.append(gap) - elif item.tag == 'entry': - producer = byid[item.get('producer')] - service = read_property(producer, 'mlt_service') - available_range = otio.opentime.TimeRange.range_from_start_end_time( - start_time=time(producer.get('in'), rate), - end_time_exclusive=( - time(producer.get('out'), rate) - + otio.opentime.RationalTime(1, rate) - ), - ) - source_range = otio.opentime.TimeRange.range_from_start_end_time( - start_time=time(item.get('in'), rate), - end_time_exclusive=( - time(item.get('out'), rate) - + otio.opentime.RationalTime(1, rate) - ), - ) - # media reference clip - reference = None - if service in ['avformat', 'avformat-novalidate', 'qimage']: - reference = otio.schema.ExternalReference( - target_url=read_property( - producer, 'kdenlive:originalurl') or - read_property(producer, 'resource'), - available_range=available_range) - elif service == 'color': - reference = otio.schema.GeneratorReference( - generator_kind='SolidColor', - parameters={'color': read_property(producer, 'resource')}, - available_range=available_range) - clip = otio.schema.Clip( - name=read_property(producer, 'kdenlive:clipname'), - source_range=source_range, - media_reference=reference or otio.schema.MissingReference()) - for effect in item.findall('filter'): - kdenlive_id = read_property(effect, 'kdenlive_id') - if kdenlive_id in ['fadein', 'fade_from_black', - 'fadeout', 'fade_to_black']: - clip.effects.append(otio.schema.Effect( - effect_name=kdenlive_id, - metadata={'duration': - time(effect.get('out'), rate) - - time(effect.get('in', - producer.get('in')), rate)})) - elif kdenlive_id in ['volume', 'brightness']: - clip.effects.append(otio.schema.Effect( - effect_name=kdenlive_id, - metadata={'keyframes': read_keyframes( - read_property(effect, 'level'), rate)})) - track.append(clip) - timeline.tracks.append(track) - - for transition in maintractor.findall('transition'): - kdenlive_id = read_property(transition, 'kdenlive_id') - if kdenlive_id == 'wipe': - timeline.tracks[int(read_property(transition, 'b_track')) - 1].append( - otio.schema.Transition( - transition_type=otio.schema.TransitionTypes.SMPTE_Dissolve, - in_offset=time(transition.get('in'), rate), - out_offset=time(transition.get('out'), rate))) - - return timeline - - -def write_property(element, name, value): - """Store an MLT property - value contained in a "property" sub element - with defined "name" attribute""" - property = ET.SubElement(element, 'property', {'name': name}) - property.text = value - - -def clock(time): - """Encode time to an MLT timecode string - after format hours:minutes:seconds.floatpart""" - frames = time.value - hours = int(frames / (time.rate * 3600)) - frames -= int(hours * 3600 * time.rate) - mins = int(frames / (time.rate * 60)) - frames -= int(mins * 60 * time.rate) - secs = frames / time.rate - return "%02d:%02d:%06.3f" % (hours, mins, secs) - - -def write_keyframes(kfdict): - """Build a MLT keyframe string""" - return ';'.join('{}={}'.format(t, v) - for t, v in kfdict.items()) - - -def write_to_string(input_otio): - """Write a timeline to Kdenlive project - Re-creating the bin storing all used source clips - and constructing the tracks""" - if not isinstance(input_otio, otio.schema.Timeline) and len(input_otio) > 1: - print('WARNING: Only one timeline supported, using the first one.') - input_otio = input_otio[0] - # Project header & metadata - mlt = ET.Element( - 'mlt', - dict( - version="6.23.0", - title=input_otio.name, - LC_NUMERIC="C", - producer="main_bin", - ), - ) - rate = input_otio.duration().rate - (rate_num, rate_den) = { - 23.98: (24000, 1001), - 29.97: (30000, 1001), - 59.94: (60000, 1001) - }.get(round(float(rate), 2), (int(rate), 1)) - ET.SubElement( - mlt, 'profile', - dict( - description='HD 1080p {} fps'.format(rate), - frame_rate_num=str(rate_num), - frame_rate_den=str(rate_den), - width='1920', - height='1080', - display_aspect_num='16', - display_aspect_den='9', - sample_aspect_num='1', - sample_aspect_den='1', - colorspace='709', - progressive='1', - ), - ) - - # Build media library, indexed by url - main_bin = ET.Element('playlist', dict(id='main_bin')) - write_property(main_bin, 'kdenlive:docproperties.decimalPoint', '.') - write_property(main_bin, 'kdenlive:docproperties.version', '0.98') - write_property(main_bin, 'xml_retain', '1') - - producer_count = 0 - - # Build media library, indexed by url - main_bin = ET.Element('playlist', dict(id='main_bin')) - write_property(main_bin, 'kdenlive:docproperties.decimalPoint', '.') - write_property(main_bin, 'kdenlive:docproperties.version', '0.98') - write_property(main_bin, 'xml_retain', '1') - media_prod = {} - for clip in input_otio.each_clip(): - producer, producer_count = _make_producer( - producer_count, clip, mlt, rate, media_prod - ) - if producer is not None: - producer_id = producer.get('id') - kdenlive_id = read_property(producer, 'kdenlive:id') - entry_in = producer.get('in') - entry_out = producer.get('out') - entry = ET.SubElement( - main_bin, 'entry', - { - 'producer': producer_id, - 'in': entry_in, - 'out': entry_out, - }, - ) - write_property(entry, 'kdenlive:id', kdenlive_id) - - # Substitute source clip to be referred to when meeting an unsupported clip - unsupported = ET.SubElement( - mlt, 'producer', - { - 'id': 'unsupported', - 'in': '0', - 'out': '10000', - }, - ) - write_property(unsupported, 'mlt_service', 'qtext') - write_property(unsupported, 'family', 'Courier') - write_property(unsupported, 'fgcolour', '#ff808080') - write_property(unsupported, 'bgcolour', '#00000000') - write_property(unsupported, 'text', 'Unsupported clip type') - write_property(unsupported, 'kdenlive:id', '3') - - entry = ET.SubElement( - main_bin, 'entry', - dict(producer='unsupported'), - ) - write_property(entry, 'kdenlive:id', '3') - - mlt.append(main_bin) - - # Background clip - black = ET.SubElement(mlt, 'producer', {'id': 'black_track'}) - write_property(black, 'resource', 'black') - write_property(black, 'mlt_service', 'color') - - # Timeline & tracks - maintractor = ET.Element('tractor', {'global_feed': '1'}) - ET.SubElement(maintractor, 'track', {'producer': 'black_track'}) - - for i, track in enumerate(input_otio.tracks): - is_audio = track.kind == otio.schema.TrackKind.Audio - - tractor_id = 'tractor{}'.format(i) - subtractor = ET.Element('tractor', dict(id=tractor_id)) - write_property(subtractor, 'kdenlive:track_name', track.name) - - ET.SubElement( - maintractor, 'track', dict(producer=tractor_id) - ) - - playlist = _make_playlist( - 2 * i, - "video" if is_audio else "audio", - subtractor, - mlt, - ) - dummy_playlist = _make_playlist(2 * i + 1, "both", subtractor, mlt) - - if is_audio: - write_property(subtractor, 'kdenlive:audio_track', '1') - write_property(playlist, 'kdenlive:audio_track', '1') - - # Track playlist - for item in track: - if isinstance(item, otio.schema.Gap): - ET.SubElement( - playlist, 'blank', dict(length=clock(item.duration())) - ) - elif isinstance(item, otio.schema.Clip): - producer_id = "unsupported" - reset_range = otio.opentime.TimeRange( - start_time=otio.opentime.RationalTime(0), - duration=item.source_range.duration, - ) - clip_in = reset_range.start_time - clip_out = reset_range.end_time_inclusive() - kdenlive_id = "3" - if isinstance(item.media_reference, - otio.schema.ExternalReference): - key = _prod_key_from_item(item, is_audio) - producer_id, kdenlive_id = media_prod[ - key - ] - speed = key[2] - if speed is None: - speed = 1 - source_range = otio.opentime.TimeRange( - otio.opentime.RationalTime( - item.source_range.start_time.value / speed, - item.source_range.start_time.rate, - ), - item.source_range.duration, - ) - clip_in = source_range.start_time - clip_out = source_range.end_time_inclusive() - elif ( - isinstance(item.media_reference, - otio.schema.GeneratorReference) - and item.media_reference.generator_kind == 'SolidColor' - ): - producer_id, kdenlive_id = media_prod[ - ( - "color", - item.media_reference.parameters['color'], - None, - None, - ) - ] - - entry = ET.SubElement( - playlist, 'entry', - { - 'producer': producer_id, - 'in': clock(clip_in), - 'out': clock(clip_out), - }, - ) - write_property(entry, 'kdenlive:id', kdenlive_id) - - # Clip effects - for effect in item.effects: - kid = effect.effect_name - if kid in ['fadein', 'fade_from_black']: - filt = ET.SubElement( - entry, 'filter', - { - "in": clock(clip_in), - "out": clock(clip_in + effect.metadata['duration']), - }, - ) - write_property(filt, 'kdenlive_id', kid) - write_property(filt, 'end', '1') - if kid == 'fadein': - write_property(filt, 'mlt_service', 'volume') - write_property(filt, 'gain', '0') - else: - write_property(filt, 'mlt_service', 'brightness') - write_property(filt, 'start', '0') - elif effect.effect_name in ['fadeout', 'fade_to_black']: - filt = ET.SubElement( - entry, 'filter', - { - "in": clock(clip_out - effect.metadata['duration']), - "out": clock(clip_out), - }, - ) - write_property(filt, 'kdenlive_id', kid) - write_property(filt, 'end', '0') - if kid == 'fadeout': - write_property(filt, 'mlt_service', 'volume') - write_property(filt, 'gain', '1') - else: - write_property(filt, 'mlt_service', 'brightness') - write_property(filt, 'start', '1') - elif effect.effect_name in ['volume', 'brightness']: - filt = ET.SubElement(entry, 'filter') - write_property(filt, 'kdenlive_id', kid) - write_property(filt, 'mlt_service', kid) - write_property(filt, 'level', - write_keyframes(effect.metadata['keyframes'])) - - elif isinstance(item, otio.schema.Transition): - print('Transitions handling to be added') - - mlt.extend((playlist, dummy_playlist, subtractor)) - - mlt.append(maintractor) - - return minidom.parseString(ET.tostring(mlt)).toprettyxml( - encoding=sys.getdefaultencoding(), - ).decode(sys.getdefaultencoding()) - - -def _make_playlist(count, hide, subtractor, mlt): - playlist_id = 'playlist{}'.format(count) - playlist = ET.Element( - 'playlist', - dict(id=playlist_id), - ) - ET.SubElement( - subtractor, 'track', - dict( - producer=playlist_id, - hide=hide, - ), - ) - return playlist - - -def _decode_media_reference_url(url): - return unquote(urlparse(url).path) - - -def _make_producer(count, item, mlt, frame_rate, media_prod, speed=None, is_audio=None): - producer = None - service, resource, effect_speed, _ = _prod_key_from_item(item, is_audio) - if service and resource: - producer_id = "producer{}".format(count) - kdenlive_id = str(count + 4) # unsupported starts with id 3 - - key = (service, resource, speed, is_audio) - # check not already in our library - if key not in media_prod: - # add ids to library - media_prod[key] = producer_id, kdenlive_id - producer = ET.SubElement( - mlt, 'producer', - { - 'id': producer_id, - 'in': clock(item.media_reference.available_range.start_time), - 'out': clock( - item.media_reference.available_range.end_time_inclusive() - ), - }, - ) - write_property(producer, 'global_feed', '1') - duration = item.media_reference.available_range.duration.rescaled_to( - frame_rate - ) - if speed is not None: - kdenlive_id = media_prod[(service, resource, None, None)][1] - write_property(producer, 'mlt_service', "timewarp") - write_property(producer, 'resource', ":".join((str(speed), resource))) - write_property(producer, 'warp_speed', str(speed)) - write_property(producer, 'warp_resource', resource) - write_property(producer, 'warp_pitch', "0") - write_property(producer, 'set.test_audio', "0" if is_audio else "1") - write_property(producer, 'set.test_image', "1" if is_audio else "0") - start_time = otio.opentime.RationalTime( - round( - item.media_reference.available_range.start_time.value - / speed - ), - item.media_reference.available_range.start_time.rate, - ) - duration = otio.opentime.RationalTime( - round(duration.value / speed), - duration.rate, - ) - producer.set( - "out", - clock( - otio.opentime.TimeRange( - start_time, - duration, - ).end_time_inclusive() - ), - ) - else: - write_property(producer, 'mlt_service', service) - write_property(producer, 'resource', resource) - if item.name: - write_property(producer, 'kdenlive:clipname', item.name) - write_property( - producer, 'length', - str(int(duration.value)), - ) - write_property(producer, 'kdenlive:id', kdenlive_id) - - count += 1 - - # create time warped version - if speed is None and effect_speed is not None: - # Make video resped producer - _, count = _make_producer( - count, item, mlt, frame_rate, media_prod, effect_speed, False - ) - # Make audio resped producer - _, count = _make_producer( - count, item, mlt, frame_rate, media_prod, effect_speed, True - ) - - return producer, count - - -def _prod_key_from_item(item, is_audio): - service = None - resource = None - speed = None - if isinstance( - item.media_reference, - (otio.schema.ExternalReference, otio.schema.MissingReference), - ): - if isinstance(item.media_reference, otio.schema.ExternalReference): - resource = _decode_media_reference_url(item.media_reference.target_url) - elif isinstance(item.media_reference, otio.schema.MissingReference): - resource = item.name - - ext_lower = os.path.splitext(resource)[1].lower() - if ext_lower == ".kdenlive": - service = "xml" - elif ext_lower in ( - ".png", ".jpg", ".jpeg" - ): - service = "qimage" - else: - service = "avformat-novalidate" - - for effect in item.effects: - if isinstance(effect, otio.schema.LinearTimeWarp): - if speed is None: - speed = 1 - speed *= effect.time_scalar - elif ( - isinstance(item.media_reference, otio.schema.GeneratorReference) - and item.media_reference.generator_kind == 'SolidColor' - ): - service = 'color' - resource = item.media_reference.parameters['color'] - return service, resource, speed, None if speed is None else is_audio - - -if __name__ == '__main__': - timeline = read_from_string( - open('tests/sample_data/kdenlive_example.kdenlive', 'r').read()) - print(str(timeline).replace('otio.schema', "\notio.schema")) - xml = write_to_string(timeline) - print(str(xml)) diff --git a/contrib/opentimelineio_contrib/adapters/maya_sequencer.py b/contrib/opentimelineio_contrib/adapters/maya_sequencer.py index aded36fb5..6ea9bd6dd 100644 --- a/contrib/opentimelineio_contrib/adapters/maya_sequencer.py +++ b/contrib/opentimelineio_contrib/adapters/maya_sequencer.py @@ -1,26 +1,5 @@ -# +# SPDX-License-Identifier: Apache-2.0 # Copyright Contributors to the OpenTimelineIO project -# -# Licensed under the Apache License, Version 2.0 (the "Apache License") -# with the following modification; you may not use this file except in -# compliance with the Apache License and the following modification to it: -# Section 6. Trademarks. is deleted and replaced with: -# -# 6. Trademarks. This License does not grant permission to use the trade -# names, trademarks, service marks, or product names of the Licensor -# and its affiliates, except as required to comply with Section 4(c) of -# the License and to reproduce the content of the NOTICE file. -# -# You may obtain a copy of the Apache License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the Apache License with the above modification is -# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -# KIND, either express or implied. See the Apache License for the specific -# language governing permissions and limitations under the Apache License. -# """Maya Sequencer Adapter Harness""" diff --git a/contrib/opentimelineio_contrib/adapters/rv.py b/contrib/opentimelineio_contrib/adapters/rv.py deleted file mode 100644 index a0d1f2df5..000000000 --- a/contrib/opentimelineio_contrib/adapters/rv.py +++ /dev/null @@ -1,446 +0,0 @@ -# -# Copyright Contributors to the OpenTimelineIO project -# -# Licensed under the Apache License, Version 2.0 (the "Apache License") -# with the following modification; you may not use this file except in -# compliance with the Apache License and the following modification to it: -# Section 6. Trademarks. is deleted and replaced with: -# -# 6. Trademarks. This License does not grant permission to use the trade -# names, trademarks, service marks, or product names of the Licensor -# and its affiliates, except as required to comply with Section 4(c) of -# the License and to reproduce the content of the NOTICE file. -# -# You may obtain a copy of the Apache License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the Apache License with the above modification is -# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -# KIND, either express or implied. See the Apache License for the specific -# language governing permissions and limitations under the Apache License. -# - -"""RvSession Adapter harness""" - -import subprocess -import os -import copy -import json - -import opentimelineio as otio - - -# exception class @{ -class NoMappingForOtioTypeError(otio.exceptions.OTIOError): - pass -# @} - - -def write_to_file(input_otio, filepath): - if "OTIO_RV_PYTHON_BIN" not in os.environ: - raise RuntimeError( - "'OTIO_RV_PYTHON_BIN' not set, please set this to path to " - "py-interp within the RV installation." - ) - - if "OTIO_RV_PYTHON_LIB" not in os.environ: - raise RuntimeError( - "'OTIO_RV_PYTHON_LIB' not set, please set this to path to python " - "directory within the RV installation." - ) - - # the adapter generates a simple JSON blob that gets turned into calls into - # the RV API. - simplified_data = generate_simplified_json(input_otio) - - base_environment = copy.deepcopy(os.environ) - - base_environment['PYTHONPATH'] = ( - os.pathsep.join( - [ - # ensure that the rv adapter is on the pythonpath - os.path.dirname(__file__), - ] - ) - ) - - proc = subprocess.Popen( - [ - base_environment["OTIO_RV_PYTHON_BIN"], - '-m', - 'extern_rv', - filepath - ], - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - stdin=subprocess.PIPE, - env=base_environment - ) - - # for debugging - # with open("/var/tmp/test.json", 'w') as fo: - # fo.write(json.dumps(simplified_data, sort_keys=True, indent=4)) - - # If the subprocess fails before writing to stdin is complete, python will - # throw a IOError exception. If it fails after writing to stdin, there - # won't be an exception. Either way, the return code will be non-0 so the - # rest of the code should catch the error case and print the (presumably) - # helpful message from the subprocess. - try: - proc.stdin.write(json.dumps(simplified_data).encode()) - except IOError: - pass - - out, err = proc.communicate() - out = out.decode() - err = err.decode() - - if out.strip(): - print("stdout: {}".format(out)) - if err.strip(): - print("stderr: {}".format(err)) - - if proc.returncode: - raise RuntimeError( - "ERROR: extern_rv (called through the rv session file adapter) " - "failed. stderr output: " + err - ) - - -# real entry point for translator -def generate_simplified_json(input_otio): - session_json = { - "nodes": [], - } - - write_otio(input_otio, session_json) - - return session_json - - -def write_otio(otio_obj, to_session, track_kind=None): - WRITE_TYPE_MAP = { - otio.schema.Timeline: _write_timeline, - otio.schema.Stack: _write_stack, - otio.schema.Track: _write_track, - otio.schema.Clip: _write_item, - otio.schema.Gap: _write_item, - otio.schema.Transition: _write_transition, - otio.schema.SerializableCollection: _write_collection, - } - - if type(otio_obj) in WRITE_TYPE_MAP: - return WRITE_TYPE_MAP[type(otio_obj)](otio_obj, to_session, track_kind) - - raise NoMappingForOtioTypeError( - str(type(otio_obj)) + " on object: {}".format(otio_obj) - ) - - -def _add_node(to_session, kind, name=""): - new_node = { - "kind": kind, - "name": name, - "properties": [], - "inputs": [], - "commands": [], - "node_index": len(to_session['nodes']), - } - to_session['nodes'].append(new_node) - return new_node - - -def _add_input(to_node, input_node): - to_node["inputs"].append(input_node["node_index"]) - - -def _add_property(to_node, args): - to_node['properties'].append(args) - - -def _add_command(to_node, command_name, args): - to_node["commands"].append((command_name, args)) - - -def _write_dissolve(pre_item, in_dissolve, post_item, to_session, track_kind=None): - new_trx = _add_node(to_session, "CrossDissolve", str(in_dissolve.name)) - - rate = pre_item.trimmed_range().duration.rate - _add_property( - new_trx, [ - "CrossDissolve", - "", - "parameters", - "startFrame", - "rvSession.gto.FLOAT", - 1.0 - ] - ) - _add_property( - new_trx, - [ - "CrossDissolve", - "", - "parameters", - "numFrames", - "rvSession.gto.FLOAT", - int( - ( - in_dissolve.in_offset - + in_dissolve.out_offset - ).rescaled_to(rate).value - ) - ] - ) - _add_property( - new_trx, - [ - "CrossDissolve", - "", - "output", - "fps", - "rvSession.gto.FLOAT", - rate - ] - ) - - pre_item_rv = write_otio(pre_item, to_session, track_kind) - _add_input(new_trx, pre_item_rv) - - post_item_rv = write_otio(post_item, to_session, track_kind) - node_to_insert = post_item_rv - - if ( - hasattr(pre_item, "media_reference") - and pre_item.media_reference - and pre_item.media_reference.available_range - and hasattr(post_item, "media_reference") - and post_item.media_reference - and post_item.media_reference.available_range - and ( - post_item.media_reference.available_range.start_time.rate != - pre_item.media_reference.available_range.start_time.rate - ) - ): - # write a retime to make sure post_item is in the timebase of pre_item - rt_node = _add_node(to_session, "Retime", "transition_retime") - _add_command( - rt_node, - "setTargetFps", - pre_item.media_reference.available_range.start_time.rate - ) - - post_item_rv = write_otio(post_item, to_session, track_kind) - - _add_input(rt_node, post_item_rv) - node_to_insert = rt_node - - _add_input(new_trx, node_to_insert) - - return new_trx - - -def _write_transition( - pre_item, - in_trx, - post_item, - to_session, - track_kind=None -): - trx_map = { - otio.schema.TransitionTypes.SMPTE_Dissolve: _write_dissolve, - } - - if in_trx.transition_type not in trx_map: - return - - return trx_map[in_trx.transition_type]( - pre_item, - in_trx, - post_item, - to_session, - track_kind - ) - - -def _write_stack(in_stack, to_session, track_kind=None): - new_stack = _add_node(to_session, "Stack", str(in_stack.name) or "tracks") - - for seq in in_stack: - result = write_otio(seq, to_session, track_kind) - if result: - _add_input(new_stack, result) - - return new_stack - - -def _write_track(in_seq, to_session, _=None): - new_seq = _add_node(to_session, "Sequence", str(in_seq.name) or "track") - - items_to_serialize = otio.algorithms.track_with_expanded_transitions( - in_seq - ) - - track_kind = in_seq.kind - - for thing in items_to_serialize: - if isinstance(thing, tuple): - result = _write_transition( - *thing, - to_session=to_session, - track_kind=track_kind - ) - elif thing.duration().value == 0: - continue - else: - result = write_otio(thing, to_session, track_kind) - - if result: - _add_input(new_seq, result) - - return new_seq - - -def _write_timeline(tl, to_session, _=None): - result = write_otio(tl.tracks, to_session) - return result - - -def _write_collection(collection, to_session, track_kind=None): - results = [] - for item in collection: - result = write_otio(item, to_session, track_kind) - if result: - results.append(result) - - if results: - return results[0] - - -def _create_media_reference(item, src, track_kind=None): - if hasattr(item, "media_reference") and item.media_reference: - if isinstance(item.media_reference, otio.schema.ExternalReference): - media = [str(item.media_reference.target_url)] - - if track_kind == otio.schema.TrackKind.Audio: - # Create blank video media to accompany audio for valid source - blank = "{},start={},end={},fps={}.movieproc".format( - "blank", - item.available_range().start_time.value, - item.available_range().end_time_inclusive().value, - item.available_range().duration.rate - ) - # Inserting blank media here forces all content to only - # produce audio. We do it twice in case we look at this in - # stereo - media = [blank, blank] + media - - _add_command(src, "setMedia", media) - return True - - elif isinstance(item.media_reference, otio.schema.ImageSequenceReference): - frame_sub = "%0{n}d".format( - n=item.media_reference.frame_zero_padding - ) - - media = [ - str(item.media_reference.abstract_target_url(symbol=frame_sub)) - ] - - _add_command(src, "setMedia", media) - - return True - - elif isinstance(item.media_reference, otio.schema.GeneratorReference): - if item.media_reference.generator_kind == "SMPTEBars": - kind = "smptebars" - _add_command( - src, - "setMedia", - [ - "{},start={},end={},fps={}.movieproc".format( - kind, - item.available_range().start_time.value, - item.available_range().end_time_inclusive().value, - item.available_range().duration.rate - ) - ] - ) - return True - - return False - - -def _write_item(it, to_session, track_kind=None): - new_item = _add_node(to_session, "Source", str(it.name) or "clip") - - if it.metadata: - - _add_property( - new_item, - - # arguments to property - [ - "RVSourceGroup", - "source", - "otio", - "metadata", - "rvSession.gto.STRING", - # Serialize to a string as it seems gto has issues with unicode - str(otio.core.serialize_json_to_string(it.metadata, indent=-1)) - ] - ) - - range_to_read = it.trimmed_range() - - if not range_to_read: - raise otio.exceptions.OTIOError( - "No valid range on clip: {0}.".format( - str(it) - ) - ) - - in_frame = out_frame = None - if hasattr(it, "media_reference") and it.media_reference: - if isinstance(it.media_reference, otio.schema.ImageSequenceReference): - in_frame, out_frame = it.media_reference.frame_range_for_time_range( - range_to_read - ) - - if not in_frame and not out_frame: - # because OTIO has no global concept of FPS, the rate of the duration - # is used as the rate for the range of the source. - in_frame = otio.opentime.to_frames( - range_to_read.start_time, - rate=range_to_read.duration.rate - ) - out_frame = otio.opentime.to_frames( - range_to_read.end_time_inclusive(), - rate=range_to_read.duration.rate - ) - - _add_command(new_item, "setCutIn", in_frame) - _add_command(new_item, "setCutOut", out_frame) - _add_command(new_item, "setFPS", range_to_read.duration.rate) - - # if the media reference is missing - if not _create_media_reference(it, new_item, track_kind): - kind = "smptebars" - if isinstance(it, otio.schema.Gap): - kind = "blank" - _add_command( - new_item, - "setMedia", - [ - "{},start={},end={},fps={}.movieproc".format( - kind, - range_to_read.start_time.value, - range_to_read.end_time_inclusive().value, - range_to_read.duration.rate - ) - ] - ) - - return new_item diff --git a/contrib/opentimelineio_contrib/adapters/tests/__init__.py b/contrib/opentimelineio_contrib/adapters/tests/__init__.py index e69de29bb..686a8cb5f 100644 --- a/contrib/opentimelineio_contrib/adapters/tests/__init__.py +++ b/contrib/opentimelineio_contrib/adapters/tests/__init__.py @@ -0,0 +1,2 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright Contributors to the OpenTimelineIO project diff --git a/contrib/opentimelineio_contrib/adapters/tests/sample_data/generator_reference_test.otio b/contrib/opentimelineio_contrib/adapters/tests/sample_data/generator_reference_test.otio new file mode 100644 index 000000000..0d8d4e5f6 --- /dev/null +++ b/contrib/opentimelineio_contrib/adapters/tests/sample_data/generator_reference_test.otio @@ -0,0 +1,70 @@ +{ + "OTIO_SCHEMA": "Timeline.1", + "metadata": {}, + "name": "transition_test", + "tracks": { + "OTIO_SCHEMA": "Stack.1", + "children": [ + { + "OTIO_SCHEMA": "Sequence.1", + "children": [ + { + "OTIO_SCHEMA": "Clip.1", + "effects": [], + "markers": [], + "enabled": true, + "media_reference": { + "OTIO_SCHEMA" : "GeneratorReference.1", + "available_range" : { + "OTIO_SCHEMA": "TimeRange.1", + "duration": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 24, + "value": 50 + }, + "start_time": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 24, + "value": 0.0 + } + }, + "generator_kind" : "SMPTEBars", + "metadata" : {}, + "parameters" : {}, + "name" : "bars" + }, + "metadata": {}, + "name": "C", + "source_range": { + "OTIO_SCHEMA": "TimeRange.1", + "duration": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 24, + "value": 50 + }, + "start_time": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 24, + "value": 0.0 + } + } + + } + ], + "effects": [], + "kind": "Video", + "markers": [], + "enabled": true, + "metadata": {}, + "name": "Sequence1", + "source_range": null + } + ], + "effects": [], + "markers": [], + "enabled": true, + "metadata": {}, + "name": "tracks", + "source_range": null + } +} diff --git a/contrib/opentimelineio_contrib/adapters/tests/sample_data/kdenlive_example_from_fcp.xml b/contrib/opentimelineio_contrib/adapters/tests/sample_data/kdenlive_example_from_fcp.xml deleted file mode 100755 index f4afd8884..000000000 --- a/contrib/opentimelineio_contrib/adapters/tests/sample_data/kdenlive_example_from_fcp.xml +++ /dev/null @@ -1,12817 +0,0 @@ - - - - - 7a985ce9-222e-4f2d-831e-4cb791178c5d - 28982 - - 30 - TRUE - - Challenges_v004 - - - - - - - 30 - TRUE - - 00;00;00;00 - 0 - DF - - - Forest - - - - - - - - - - - - diff --git a/contrib/opentimelineio_contrib/adapters/tests/sample_data/kdenlive_example.kdenlive b/contrib/opentimelineio_contrib/adapters/tests/sample_data/kdenlive_example_v221170.kdenlive similarity index 91% rename from contrib/opentimelineio_contrib/adapters/tests/sample_data/kdenlive_example.kdenlive rename to contrib/opentimelineio_contrib/adapters/tests/sample_data/kdenlive_example_v221170.kdenlive index 739272c52..c09cb0f2e 100644 --- a/contrib/opentimelineio_contrib/adapters/tests/sample_data/kdenlive_example.kdenlive +++ b/contrib/opentimelineio_contrib/adapters/tests/sample_data/kdenlive_example_v221170.kdenlive @@ -1,10 +1,24 @@ - + 8701 pause AUD0001.OGG + 1.06667 + INVALID + 0xffffffff + 0x00000000 + 0x00000000 + 0 + left + 0 + Sans + 48 + normal + 400 + UTF-8 + avformat 1 audio fltp @@ -17,18 +31,32 @@ 0 -1 1 - avformat -1 2 4746320 2581967857de47ee2adee0965a84fd70 1 + avformat 3085 pause AUD0002.OGG + 1.06667 + INVALID + 0xffffffff + 0x00000000 + 0x00000000 + 0 + left + 0 + Sans + 48 + normal + 400 + UTF-8 + avformat 1 audio fltp @@ -41,41 +69,29 @@ 0 -1 1 - avformat Music -1 3 1686721 f5fc0d494799f2cbaabc85c758b7d6d6 1 + avformat - - 00:00:05.000 - pause - 0x2a3158ff - 1.06667 - color - Dark Blue - 00:00:05.000 - -1 - 7 - 466ccb917e30c2d39bcd72068d75e4df - - - 672 + + 979 pause - VID0003.MKV + VID0001.MKV 1 0 1 avformat-novalidate 1 2.4 - Close 2 - 9 - 6 - 2203343 - da3511cd5b12be5acb6a88fb3f789dff + Wide + 8 + 4 + 3590506 + 162662aad64e84bb9dea3270ffa9dfa7 1 2 video @@ -93,7 +109,7 @@ 0 VideoHandler Lavc58.54.100 libx264 - 00:00:24.894000000 + 00:00:37.203000000 audio fltp 48000 @@ -103,10 +119,10 @@ 0 SoundHandler Lavc58.54.100 libvorbis - 00:00:26.862000000 - iso5 + 00:00:39.159000000 512 iso6mp41 + iso5 Lavf58.29.100 12 5 @@ -119,6 +135,9 @@ mpeg 0 1 + 508 + 639 + avformat-novalidate 308 @@ -178,22 +197,23 @@ mpeg 0 1 + avformat-novalidate - - 979 + + 672 pause - VID0001.MKV + VID0003.MKV 1 0 1 avformat-novalidate 1 2.4 - Wide - 8 - 4 - 3590506 - 162662aad64e84bb9dea3270ffa9dfa7 + Close 2 + 9 + 6 + 2203343 + da3511cd5b12be5acb6a88fb3f789dff 1 2 video @@ -211,7 +231,7 @@ 0 VideoHandler Lavc58.54.100 libx264 - 00:00:37.203000000 + 00:00:24.894000000 audio fltp 48000 @@ -221,10 +241,10 @@ 0 SoundHandler Lavc58.54.100 libvorbis - 00:00:39.159000000 + 00:00:26.862000000 + iso5 512 iso6mp41 - iso5 Lavf58.29.100 12 5 @@ -237,8 +257,21 @@ mpeg 0 1 - 508 - 639 + avformat-novalidate + + + 00:00:05.000 + pause + 0x2a3158ff + 1.06667 + color + Dark Blue + 00:00:05.000 + -1 + 7 + 466ccb917e30c2d39bcd72068d75e4df + rgb + 2 125 @@ -254,7 +287,7 @@ <position x="110" y="230"> <transform>1,0,0,0,1,0,0,0,1</transform> </position> - <content line-spacing="0" shadow="0;#64000000;3;3;3" font-underline="0" box-height="70" font-outline-color="0,0,0,255" font="Lucida Sans" letter-spacing="0" font-pixel-size="60" font-italic="0" alignment="4" font-weight="50" font-outline="0" box-width="545" font-color="255,253,184,255">Testing Kdenlive...</content> + <content line-spacing="0" shadow="0;#64000000;3;3;3" font-underline="0" box-height="70" font-outline-color="0,0,0,255" font="Noto Sans" letter-spacing="0" font-pixel-size="60" font-italic="0" alignment="4" font-weight="50" font-outline="0" box-width="545" font-color="255,253,184,255">Testing Kdenlive...</content> </item> <startviewport rect="0,0,768,576"/> <endviewport rect="0,0,768,576"/> @@ -264,16 +297,18 @@ -1 10 de362639624a26f69e4278e90d059408 + Placeholder: Unsupported clip type 0 768 576 + 2 day 1 day 2 - 1 - 2 - . + 4 + -1 + 1 0 1573548516836 0 @@ -315,8 +350,8 @@ } ] - 19.11.80 - 37 + 22.11.70 (rev. e629e8bf1) + 363 dv_pal @@ -325,16 +360,7 @@ 800 1000 - 0 - 30000 - 1573548516836 - 0.99 - 1 - 3 - 0 - 867 - 6 - + 640 6 Generic (HD for web, mobile devices...) -1 @@ -354,15 +380,38 @@ 0 0 0 - untitled.webm 0 + 0 + 30000 + 1573548516836 + 1.04 + 1 + -1 + 0 + 867 + 6 + 9 + + [ + { + "comment": "Purple Marker", + "pos": 230, + "type": 0 + }, + { + "comment": "Green", + "pos": 466, + "type": 3 + } +] + 1 - - - + + + @@ -371,12 +420,17 @@ black 1 color + rgba 0 3085 pause AUD0002.OGG + 0 + -1 + 1 + avformat-novalidate 1 audio fltp @@ -386,10 +440,6 @@ Vorbis 112000 1 - 0 - -1 - 1 - avformat-novalidate -1 3 @@ -399,6 +449,7 @@ was here 0 1 + avformat-novalidate 1 @@ -420,6 +471,7 @@ 1 69 + 0 Ambience @@ -443,12 +495,18 @@ 0 audiolevel + 1 + 1 8701 pause AUD0001.OGG + 0 + -1 + 1 + avformat-novalidate 1 audio fltp @@ -458,10 +516,6 @@ Vorbis 112000 1 - 0 - -1 - 1 - avformat-novalidate -1 2 @@ -471,13 +525,13 @@ was here 0 1 + avformat-novalidate 1 2 - 0 75 20dB @@ -500,7 +554,6 @@ 2 - 0 75 20dB @@ -527,6 +580,7 @@ 1 69 + 27 @@ -549,6 +603,8 @@ 0 audiolevel + 1 + 1 @@ -614,13 +670,13 @@ 1 0 1 + avformat-novalidate 1 4 - 0 75 20dB @@ -641,28 +697,31 @@ 1 69 + 27 - + 75 20dB volume 237 1 - + -1 panner 237 0.5 1 - + 0 audiolevel + 1 + 1 @@ -728,21 +787,21 @@ 1 1 0 + avformat-novalidate 7 - - 0 + + 1 + 1 brightness fade_from_black - 1 - -1 + 00:00:00.000=0;00:00:00.360=1 4 - 0 1 00:00:06.640=1;00:00:08.440=2.02899;00:00:11.960=0.57971 @@ -759,6 +818,7 @@ 69 + 0 Wide @@ -827,6 +887,7 @@ 1 1 0 + avformat-novalidate 672 @@ -889,16 +950,17 @@ 1 1 0 + avformat-novalidate 10 - - 0 + + 1 + 1 brightness fade_from_black - 1 - -1 + 00:00:00.000=0;00:00:01.280=1 @@ -913,6 +975,7 @@ 69 + 0 Close @@ -920,7 +983,7 @@ - + @@ -934,6 +997,7 @@ mix 237 1 + 1 1 @@ -943,6 +1007,7 @@ mix 237 1 + 1 1 @@ -952,47 +1017,49 @@ mix 237 1 + 1 1 0 4 - 0 - 0 - 0 - qtblend - qtblend - 237 + 0.1 + frei0r.cairoblend 1 + 237 0 5 - 0 - 0 - 0 - qtblend - qtblend - 237 + 0.1 + frei0r.cairoblend 1 + 237 - + 75 20dB volume 237 1 - + -1 panner 237 0.5 1 - + 0 audiolevel + 1 + 1 + + + avfilter.fieldorder + 237 + tff diff --git a/contrib/opentimelineio_contrib/adapters/tests/sample_data/kdenlive_mixes_markers.kdenlive b/contrib/opentimelineio_contrib/adapters/tests/sample_data/kdenlive_mixes_markers.kdenlive new file mode 100644 index 000000000..fd0d72d65 --- /dev/null +++ b/contrib/opentimelineio_contrib/adapters/tests/sample_data/kdenlive_mixes_markers.kdenlive @@ -0,0 +1,1165 @@ + + + + + 3179 + continue + myClip.mp4 + 1 + 0 + 0 + avformat-novalidate + 2 + video + 25 + 1 + 1920 + 1080 + 0 + yuv420p + 1 + 709 + 1 + h264 + H.264 / AVC / MPEG-4 AVC / MPEG-4 part 10 + 4604188 + VideoHandler + audio + fltp + 48000 + 2 + aac + AAC (Advanced Audio Coding) + 161378 + SoundHandler + isom + 512 + isomiso2avc1mp41 + Lavf58.29.100 + 1 + 1 + 1 + 1 + SomeNiceContent + 25 + 1 + 709 + 1 + 2 + 1920 + 1080 + mpeg + 0 + 1 + 0 + -1 + 2 + 75823059 + 79625350d3beea1e8d15e38f4c323aab + 249 + -1 + [ + { + "comment": "Lila", + "pos": 1782, + "type": 0 + }, + { + "comment": "Orange", + "pos": 2899, + "type": 5 + } +] + + + + 3 + 2 + -1 + 1 + 0 + 1661003132558 + 0 + 0 + 0 + ./;GL;.LRV;./;GX;.MP4;./;GP;.LRV;./;GP;.MP4 + 0 + 0 + [ + { + "children": [ + { + "data": "1:2664", + "leaf": "clip", + "type": "Leaf" + }, + { + "data": "2:2664", + "leaf": "clip", + "type": "Leaf" + } + ], + "type": "AVSplit" + }, + { + "children": [ + { + "data": "0:1309", + "leaf": "clip", + "type": "Leaf" + }, + { + "data": "3:1297", + "leaf": "clip", + "type": "Leaf" + } + ], + "type": "AVSplit" + }, + { + "children": [ + { + "data": "0:928", + "leaf": "clip", + "type": "Leaf" + }, + { + "data": "3:928", + "leaf": "clip", + "type": "Leaf" + } + ], + "type": "AVSplit" + }, + { + "children": [ + { + "data": "1:1322", + "leaf": "clip", + "type": "Leaf" + }, + { + "data": "2:1322", + "leaf": "clip", + "type": "Leaf" + } + ], + "type": "AVSplit" + }, + { + "children": [ + { + "data": "0:2286", + "leaf": "clip", + "type": "Leaf" + }, + { + "data": "3:2184", + "leaf": "clip", + "type": "Leaf" + } + ], + "type": "AVSplit" + }, + { + "children": [ + { + "data": "0:0", + "leaf": "clip", + "type": "Leaf" + }, + { + "data": "3:0", + "leaf": "clip", + "type": "Leaf" + } + ], + "type": "AVSplit" + }, + { + "children": [ + { + "data": "1:351", + "leaf": "clip", + "type": "Leaf" + }, + { + "data": "2:351", + "leaf": "clip", + "type": "Leaf" + } + ], + "type": "AVSplit" + }, + { + "children": [ + { + "data": "1:928", + "leaf": "clip", + "type": "Leaf" + }, + { + "data": "2:928", + "leaf": "clip", + "type": "Leaf" + } + ], + "type": "AVSplit" + }, + { + "children": [ + { + "data": "0:1851", + "leaf": "clip", + "type": "Leaf" + }, + { + "data": "3:1970", + "leaf": "clip", + "type": "Leaf" + } + ], + "type": "AVSplit" + }, + { + "children": [ + { + "data": "1:1970", + "leaf": "clip", + "type": "Leaf" + }, + { + "data": "2:1970", + "leaf": "clip", + "type": "Leaf" + } + ], + "type": "AVSplit" + }, + { + "children": [ + { + "data": "0:338", + "leaf": "clip", + "type": "Leaf" + }, + { + "data": "3:338", + "leaf": "clip", + "type": "Leaf" + } + ], + "type": "AVSplit" + }, + { + "children": [ + { + "data": "0:2997", + "leaf": "clip", + "type": "Leaf" + }, + { + "data": "3:2997", + "leaf": "clip", + "type": "Leaf" + } + ], + "type": "AVSplit" + }, + { + "children": [ + { + "data": "1:2299", + "leaf": "clip", + "type": "Leaf" + }, + { + "data": "2:2299", + "leaf": "clip", + "type": "Leaf" + } + ], + "type": "AVSplit" + }, + { + "children": [ + { + "data": "1:3041", + "leaf": "clip", + "type": "Leaf" + }, + { + "data": "2:3041", + "leaf": "clip", + "type": "Leaf" + } + ], + "type": "AVSplit" + }, + { + "children": [ + { + "data": "1:0", + "leaf": "clip", + "type": "Leaf" + }, + { + "data": "2:0", + "leaf": "clip", + "type": "Leaf" + } + ], + "type": "AVSplit" + }, + { + "children": [ + { + "data": "0:2664", + "leaf": "clip", + "type": "Leaf" + }, + { + "data": "3:2664", + "leaf": "clip", + "type": "Leaf" + } + ], + "type": "AVSplit" + } +] + + [ +] + + 22.11.70 (rev. e629e8bf1) + 1974 + + + atsc_1080p_25 + + 2000 + 800 + 1000 + + 640 + 3064 + 30000 + 1.04 + 1 + -1 + 0 + 75 + 5 + + + 1 + + + + 2147483647 + continue + black + 1 + color + rgba + 0 + + + 3179 + continue + myClip.mp4 + 1 + 0 + 0 + avformat-novalidate + 1 + 1 + + 0 + -1 + 2 + 75823059 + 79625350d3beea1e8d15e38f4c323aab + 249 + 0 + [ + { + "comment": "Lila", + "pos": 1782, + "type": 0 + }, + { + "comment": "Orange", + "pos": 2899, + "type": 5 + } +] + + was here + 2 + video + 25 + 1 + 1920 + 1080 + 0 + yuv420p + 1 + 709 + 1 + h264 + H.264 / AVC / MPEG-4 AVC / MPEG-4 part 10 + 4604188 + VideoHandler + audio + fltp + 48000 + 2 + aac + AAC (Advanced Audio Coding) + 161378 + SoundHandler + isom + 512 + isomiso2avc1mp41 + Lavf58.29.100 + 1 + 1 + 25 + 1 + 709 + 1 + 2 + 1920 + 1080 + mpeg + 0 + 1 + 0 + 1 + + + 1 + + 2 + + + + 2 + + + + 2 + + + + 2 + + + + 3179 + continue + myClip.mp4 + 1 + 0 + 0 + avformat-novalidate + 1 + 1 + + 0 + -1 + 2 + 75823059 + 79625350d3beea1e8d15e38f4c323aab + 249 + 0 + [ + { + "comment": "Lila", + "pos": 1782, + "type": 0 + }, + { + "comment": "Orange", + "pos": 2899, + "type": 5 + } +] + + was here + 2 + video + 25 + 1 + 1920 + 1080 + 0 + yuv420p + 1 + 709 + 1 + h264 + H.264 / AVC / MPEG-4 AVC / MPEG-4 part 10 + 4604188 + VideoHandler + audio + fltp + 48000 + 2 + aac + AAC (Advanced Audio Coding) + 161378 + SoundHandler + isom + 512 + isomiso2avc1mp41 + Lavf58.29.100 + 1 + 1 + 25 + 1 + 709 + 1 + 2 + 1920 + 1080 + mpeg + 0 + 1 + 0 + 1 + + + 1 + + + 2 + + + + 2 + + + + 2 + + + + 2 + + + + 1 + 67 + 1 + 0 + A3 + + + + + + 0 + 1 + mix + mix + 12 + -1 + 1 + 0 + + + 0 + 1 + mix + mix + 12 + -1 + 1 + 0 + + + 0 + 1 + mix + mix + 12 + -1 + 1 + 1 + + + 0 + 1 + mix + mix + 12 + -1 + 1 + 0 + + + 0 + 1 + mix + mix + 70 + -1 + 1 + 0 + + + 75 + 20dB + volume + 237 + 1 + + + -1 + panner + 237 + 0.5 + 1 + + + 0 + audiolevel + 1 + 1 + + + + 3179 + continue + myClip.mp4 + 1 + 0 + 0 + avformat-novalidate + 1 + 1 + + 0 + -1 + 2 + 75823059 + 79625350d3beea1e8d15e38f4c323aab + 249 + 0 + [ + { + "comment": "Lila", + "pos": 1782, + "type": 0 + }, + { + "comment": "Orange", + "pos": 2899, + "type": 5 + } +] + + was here + 2 + video + 25 + 1 + 1920 + 1080 + 0 + yuv420p + 1 + 709 + 1 + h264 + H.264 / AVC / MPEG-4 AVC / MPEG-4 part 10 + 4604188 + VideoHandler + audio + fltp + 48000 + 2 + aac + AAC (Advanced Audio Coding) + 161378 + SoundHandler + isom + 512 + isomiso2avc1mp41 + Lavf58.29.100 + 1 + 1 + 25 + 1 + 709 + 1 + 2 + 1920 + 1080 + mpeg + 0 + 1 + 0 + 1 + + + 1 + + 2 + + + 2 + + + + 2 + + + 2 + + + 2 + + + 2 + + + + 2 + + + 2 + + + + 1 + + + 1 + 67 + 1 + 0 + A2 + + + + + + 75 + 20dB + volume + 237 + 1 + + + -1 + panner + 237 + 0.5 + 1 + + + 0 + audiolevel + 1 + 1 + + + + 3179 + continue + myClip.mp4 + 1 + 0 + 0 + avformat-novalidate + 1 + 1 + + 0 + -1 + 2 + 75823059 + 79625350d3beea1e8d15e38f4c323aab + 249 + 0 + was here + 2 + video + 25 + 1 + 1920 + 1080 + 0 + yuv420p + 1 + 709 + 1 + h264 + H.264 / AVC / MPEG-4 AVC / MPEG-4 part 10 + 4604188 + VideoHandler + audio + fltp + 48000 + 2 + aac + AAC (Advanced Audio Coding) + 161378 + SoundHandler + isom + 512 + isomiso2avc1mp41 + Lavf58.29.100 + 1 + 1 + 25 + 1 + 709 + 1 + 2 + 1920 + 1080 + mpeg + 0 + 1 + 1 + 0 + + + + 2 + + + 2 + + + + 2 + + + 2 + + + 2 + + + 2 + + + + 2 + + + 2 + + + + + 67 + 1 + 0 + Nothing + + + + + + + 3179 + continue + myClip.mp4 + 1 + 0 + 0 + avformat-novalidate + 1 + 1 + + 0 + -1 + 2 + 75823059 + 79625350d3beea1e8d15e38f4c323aab + 249 + 0 + was here + 2 + video + 25 + 1 + 1920 + 1080 + 0 + yuv420p + 1 + 709 + 1 + h264 + H.264 / AVC / MPEG-4 AVC / MPEG-4 part 10 + 4604188 + VideoHandler + audio + fltp + 48000 + 2 + aac + AAC (Advanced Audio Coding) + 161378 + SoundHandler + isom + 512 + isomiso2avc1mp41 + Lavf58.29.100 + 1 + 1 + 25 + 1 + 709 + 1 + 2 + 1920 + 1080 + mpeg + 0 + 1 + 1 + 0 + + + + 2 + + + + 2 + + + + 2 + + + + 2 + + + + 3179 + continue + myClip.mp4 + 1 + 0 + 0 + avformat-novalidate + 1 + 1 + + 0 + -1 + 2 + 75823059 + 79625350d3beea1e8d15e38f4c323aab + 249 + 0 + was here + 2 + video + 25 + 1 + 1920 + 1080 + 0 + yuv420p + 1 + 709 + 1 + h264 + H.264 / AVC / MPEG-4 AVC / MPEG-4 part 10 + 4604188 + VideoHandler + audio + fltp + 48000 + 2 + aac + AAC (Advanced Audio Coding) + 161378 + SoundHandler + isom + 512 + isomiso2avc1mp41 + Lavf58.29.100 + 1 + 1 + 25 + 1 + 709 + 1 + 2 + 1920 + 1080 + mpeg + 0 + 1 + 1 + 0 + + + + + 2 + + + + 2 + + + + 2 + + + + 2 + + + + 67 + 1 + 0 + With Mix + + + + + + 0 + 1 + loader + + luma + luma + 12 + 0 + 1 + 0 + 0 + + + 0 + 1 + loader + + luma + luma + 0 + 0 + 1 + 0 + 0 + + + 0 + 1 + loader + + luma + luma + 25 + 1 + 0 + 1 + 0 + + + 0 + 1 + loader + + luma + luma + 99 + 0 + 1 + 0 + 0 + + + 0 + 1 + loader + + luma + luma + 70 + 0 + 1 + 0 + 0 + + + + + + + + + + 0 + 1 + mix + mix + 237 + 1 + 1 + 1 + + + 0 + 2 + mix + mix + 237 + 1 + 1 + 1 + + + 0 + 3 + 0.1 + frei0r.cairoblend + 1 + 237 + + + 0 + 4 + 0.1 + frei0r.cairoblend + 1 + 237 + + + 75 + 20dB + volume + 237 + 1 + + + -1 + panner + 237 + 0.5 + 1 + + + 0 + audiolevel + 1 + 1 + + + diff --git a/contrib/opentimelineio_contrib/adapters/tests/sample_data/rv_metadata.otio b/contrib/opentimelineio_contrib/adapters/tests/sample_data/rv_metadata.otio deleted file mode 100644 index 12ae21e5d..000000000 --- a/contrib/opentimelineio_contrib/adapters/tests/sample_data/rv_metadata.otio +++ /dev/null @@ -1,140 +0,0 @@ -{ - "OTIO_SCHEMA": "Timeline.1", - "metadata": {}, - "name": "OTIO_Test_ppjoshm1.Exported.01", - "tracks": { - "OTIO_SCHEMA": "Stack.1", - "children": [ - { - "OTIO_SCHEMA": "Track.1", - "children": [ - { - "OTIO_SCHEMA": "Clip.1", - "effects": [], - "markers": [], - "media_reference": { - "OTIO_SCHEMA": "ExternalReference.1", - "available_range": { - "OTIO_SCHEMA": "TimeRange.1", - "duration": { - "OTIO_SCHEMA": "RationalTime.1", - "rate": 24, - "value": 192 - }, - "start_time": { - "OTIO_SCHEMA": "RationalTime.1", - "rate": 24, - "value": 1 - } - }, - "metadata": {}, - "name": null, - "target_url": "sample_data/one_clip.mov" - }, - "metadata": { - "cmx_3600": { - "comments": [ - "SOURCE FILE: ZZ100_507C.LAY2.01" - ], - "reel": "ZZ100_50" - } - }, - "name": "ppjoshm_1 (SIM1)", - "source_range": { - "OTIO_SCHEMA": "TimeRange.1", - "duration": { - "OTIO_SCHEMA": "RationalTime.1", - "rate": 24.0, - "value": 10 - }, - "start_time": { - "OTIO_SCHEMA": "RationalTime.1", - "rate": 24.0, - "value": 101 - } - } - } - ], - "effects": [], - "kind": "Video", - "markers": [], - "metadata": {}, - "name": "TimelineMobSlot", - "source_range": null - }, - { - "OTIO_SCHEMA": "Track.1", - "children": [ - { - "OTIO_SCHEMA": "Clip.1", - "effects": [], - "markers": [], - "media_reference": { - "OTIO_SCHEMA": "ExternalReference.1", - "available_range": { - "OTIO_SCHEMA": "TimeRange.1", - "duration": { - "OTIO_SCHEMA": "RationalTime.1", - "rate": 24, - "value": 192 - }, - "start_time": { - "OTIO_SCHEMA": "RationalTime.1", - "rate": 24, - "value": 1 - } - }, - "metadata": {}, - "name": null, - "target_url": "sample_data/one_clip.mov" - }, - "metadata": { - "example_studio": { - "OTIO_SCHEMA": "ExampleStudioMetadata.1", - "cache": { - "hitech": { - "OTIO_SCHEMA": "ExampleDatabase.1", - "shot": null, - "take": null - } - }, - "take": { - "OTIO_SCHEMA": "ExampleStudioTake.1", - "globaltake": 1, - "prod": "ppjoshm", - "shot": "ppjoshm_1", - "unit": "none" - } - } - }, - "name": "ppjoshm_1 (SIM1)", - "source_range": { - "OTIO_SCHEMA": "TimeRange.1", - "duration": { - "OTIO_SCHEMA": "RationalTime.1", - "rate": 24.0, - "value": 10 - }, - "start_time": { - "OTIO_SCHEMA": "RationalTime.1", - "rate": 24.0, - "value": 0 - } - } - } - ], - "effects": [], - "kind": "Audio", - "markers": [], - "metadata": {}, - "name": "TimelineMobSlot", - "source_range": null - } - ], - "effects": [], - "markers": [], - "metadata": {}, - "name": "tracks", - "source_range": null - } -} diff --git a/contrib/opentimelineio_contrib/adapters/tests/sample_data/rv_metadata.rv b/contrib/opentimelineio_contrib/adapters/tests/sample_data/rv_metadata.rv deleted file mode 100644 index 1ebd16774..000000000 --- a/contrib/opentimelineio_contrib/adapters/tests/sample_data/rv_metadata.rv +++ /dev/null @@ -1,111 +0,0 @@ -GTOa (4) - -rv : RVSession (4) -{ - session - { - string viewNode = "Stack000000" - } - - writer - { - string name = "rvSession.py" - string version = "0.5" - } -} - -connections : connection (2) -{ - evaluation - { - string[2] connections = [ [ "sourceGroup000001" "Sequence000001" ] [ "sourceGroup000000" "Sequence000000" ] [ "Sequence000000" "Stack000000" ] [ "Sequence000001" "Stack000000" ] ] - } -} - -Sequence000000 : RVSequenceGroup (1) -{ - ui - { - string name = "TimelineMobSlot" - } -} - -Sequence000001 : RVSequenceGroup (1) -{ - ui - { - string name = "TimelineMobSlot" - } -} - -Stack000000 : RVStackGroup (1) -{ - ui - { - string name = "tracks" - } -} - -sourceGroup000000 : RVSourceGroup (1) -{ - ui - { - string name = "ppjoshm_1 (SIM1)" - } -} - -sourceGroup000000_source : RVFileSource (1) -{ - cut - { - int in = 101 - int out = 110 - } - - group - { - float fps = 24 - } - - media - { - string movie = "sample_data/one_clip.mov" - } - - otio - { - string metadata = "{\"cmx_3600\":{\"comments\":[\"SOURCE FILE: ZZ100_507C.LAY2.01\"],\"reel\":\"ZZ100_50\"}}" - } -} - -sourceGroup000001 : RVSourceGroup (1) -{ - ui - { - string name = "ppjoshm_1 (SIM1)" - } -} - -sourceGroup000001_source : RVFileSource (1) -{ - cut - { - int in = 0 - int out = 9 - } - - group - { - float fps = 24 - } - - media - { - string movie = [ "blank,start=1.0,end=192.0,fps=24.0.movieproc" "blank,start=1.0,end=192.0,fps=24.0.movieproc" "sample_data/one_clip.mov" ] - } - - otio - { - string metadata = "{\"example_studio\":{\"OTIO_SCHEMA\":\"ExampleStudioMetadata.1\",\"cache\":{\"hitech\":{\"OTIO_SCHEMA\":\"ExampleDatabase.1\",\"shot\":null,\"take\":null}},\"take\":{\"OTIO_SCHEMA\":\"ExampleStudioTake.1\",\"globaltake\":1,\"prod\":\"ppjoshm\",\"shot\":\"ppjoshm_1\",\"unit\":\"none\"}}}" - } -} diff --git a/contrib/opentimelineio_contrib/adapters/tests/sample_data/screening_example.rv b/contrib/opentimelineio_contrib/adapters/tests/sample_data/screening_example.rv deleted file mode 100644 index e526521fb..000000000 --- a/contrib/opentimelineio_contrib/adapters/tests/sample_data/screening_example.rv +++ /dev/null @@ -1,327 +0,0 @@ -GTOa (4) - -rv : RVSession (4) -{ - session - { - string viewNode = "Stack000000" - } - - writer - { - string name = "rvSession.py" - string version = "0.5" - } -} - -connections : connection (2) -{ - evaluation - { - string[2] connections = [ [ "Sequence000000" "Stack000000" ] [ "sourceGroup000000" "Sequence000000" ] [ "sourceGroup000001" "Sequence000000" ] [ "sourceGroup000002" "Sequence000000" ] [ "sourceGroup000003" "Sequence000000" ] [ "sourceGroup000004" "Sequence000000" ] [ "sourceGroup000005" "Sequence000000" ] [ "sourceGroup000006" "Sequence000000" ] [ "sourceGroup000007" "Sequence000000" ] [ "sourceGroup000008" "Sequence000000" ] ] - } -} - -Sequence000000 : RVSequenceGroup (1) -{ - ui - { - string name = "V" - } -} - -Stack000000 : RVStackGroup (1) -{ - ui - { - string name = "tracks" - } -} - -sourceGroup000000 : RVSourceGroup (1) -{ - ui - { - string name = "ZZ100_501 (LAY3)" - } -} - -sourceGroup000000_source : RVFileSource (1) -{ - cut - { - int in = 86501 - int out = 86531 - } - - group - { - float fps = 24 - } - - media - { - string movie = "smptebars,start=86501.0,end=86531.0,fps=24.0.movieproc" - } - - otio - { - string metadata = "{\"cmx_3600\":{\"comments\":[\"SOURCE FILE: ZZ100_501.LAY3.01\"],\"reel\":\"ZZ100_50\"}}" - } -} - -sourceGroup000001 : RVSourceGroup (1) -{ - ui - { - string name = "ZZ100_502A (LAY3)" - } -} - -sourceGroup000001_source : RVFileSource (1) -{ - cut - { - int in = 86557 - int out = 86606 - } - - group - { - float fps = 24 - } - - media - { - string movie = "smptebars,start=86557.0,end=86606.0,fps=24.0.movieproc" - } - - otio - { - string metadata = "{\"cmx_3600\":{\"comments\":[\"SOURCE FILE: ZZ100_502A.LAY3.02\"],\"reel\":\"ZZ100_50\"}}" - } -} - -sourceGroup000002 : RVSourceGroup (1) -{ - ui - { - string name = "ZZ100_503A (LAY1)" - } -} - -sourceGroup000002_source : RVFileSource (1) -{ - cut - { - int in = 86601 - int out = 86628 - } - - group - { - float fps = 24 - } - - media - { - string movie = "smptebars,start=86601.0,end=86628.0,fps=24.0.movieproc" - } - - otio - { - string metadata = "{\"cmx_3600\":{\"comments\":[\"SOURCE FILE: ZZ100_503A.LAY1.01\"],\"reel\":\"ZZ100_50\"}}" - } -} - -sourceGroup000003 : RVSourceGroup (1) -{ - ui - { - string name = "ZZ100_504C (LAY1)" - } -} - -sourceGroup000003_source : RVFileSource (1) -{ - cut - { - int in = 86641 - int out = 86755 - } - - group - { - float fps = 24 - } - - media - { - string movie = "smptebars,start=86641.0,end=86755.0,fps=24.0.movieproc" - } - - otio - { - string metadata = "{\"cmx_3600\":{\"comments\":[\"SOURCE FILE: ZZ100_504C.LAY1.02\"],\"reel\":\"ZZ100_50\"}}" - } -} - -sourceGroup000004 : RVSourceGroup (1) -{ - ui - { - string name = "ZZ100_504B (LAY1)" - } -} - -sourceGroup000004_source : RVFileSource (1) -{ - cut - { - int in = 86753 - int out = 86853 - } - - group - { - float fps = 24 - } - - media - { - string movie = "smptebars,start=86753.0,end=86853.0,fps=24.0.movieproc" - } - - otio - { - string metadata = "{\"cmx_3600\":{\"comments\":[\"SOURCE FILE: ZZ100_504B.LAY1.02\"],\"reel\":\"ZZ100_50\"}}" - } -} - -sourceGroup000005 : RVSourceGroup (1) -{ - ui - { - string name = "ZZ100_507C (LAY2)" - } -} - -sourceGroup000005_source : RVFileSource (1) -{ - cut - { - int in = 86501 - int out = 86661 - } - - group - { - float fps = 24 - } - - media - { - string movie = "smptebars,start=86501.0,end=86661.0,fps=24.0.movieproc" - } - - otio - { - string metadata = "{\"cmx_3600\":{\"comments\":[\"SOURCE FILE: ZZ100_507C.LAY2.01\"],\"reel\":\"ZZ100_50\"}}" - } -} - -sourceGroup000006 : RVSourceGroup (1) -{ - ui - { - string name = "ZZ100_508 (LAY2)" - } -} - -sourceGroup000006_source : RVFileSource (1) -{ - cut - { - int in = 86628 - int out = 86797 - } - - group - { - float fps = 24 - } - - media - { - string movie = "smptebars,start=86628.0,end=86797.0,fps=24.0.movieproc" - } - - otio - { - string metadata = "{\"cmx_3600\":{\"comments\":[\"SOURCE FILE: ZZ100_508.LAY2.02\"],\"reel\":\"ZZ100_50\"}}" - } -} - -sourceGroup000007 : RVSourceGroup (1) -{ - ui - { - string name = "ZZ100_510 (LAY1)" - } -} - -sourceGroup000007_source : RVFileSource (1) -{ - cut - { - int in = 86722 - int out = 86857 - } - - group - { - float fps = 24 - } - - media - { - string movie = "smptebars,start=86722.0,end=86857.0,fps=24.0.movieproc" - } - - otio - { - string metadata = "{\"cmx_3600\":{\"comments\":[\"SOURCE FILE: ZZ100_510.LAY1.02\"],\"reel\":\"ZZ100_51\"}}" - } -} - -sourceGroup000008 : RVSourceGroup (1) -{ - ui - { - string name = "ZZ100_510B (LAY1)" - } -} - -sourceGroup000008_source : RVFileSource (1) -{ - cut - { - int in = 86501 - int out = 86757 - } - - group - { - float fps = 24 - } - - media - { - string movie = "smptebars,start=86501.0,end=86757.0,fps=24.0.movieproc" - } - - otio - { - string metadata = "{\"cmx_3600\":{\"comments\":[\"AVX2 EFFECT, RESIZE\",\"SOURCE FILE: ZZ100_510B.LAY1.02\"],\"reel\":\"ZZ100_51\"}}" - } -} diff --git a/contrib/opentimelineio_contrib/adapters/tests/sample_data/transition_test.rv b/contrib/opentimelineio_contrib/adapters/tests/sample_data/transition_test.rv deleted file mode 100644 index 3b382d5f3..000000000 --- a/contrib/opentimelineio_contrib/adapters/tests/sample_data/transition_test.rv +++ /dev/null @@ -1,339 +0,0 @@ -GTOa (4) - -rv : RVSession (4) -{ - session - { - string viewNode = "Stack000000" - } - - writer - { - string name = "rvSession.py" - string version = "0.5" - } -} - -connections : connection (2) -{ - evaluation - { - string[2] connections = [ [ "Sequence000000" "Stack000000" ] [ "sourceGroup000007" "CrossDissolve000002" ] [ "sourceGroup000008" "CrossDissolve000002" ] [ "sourceGroup000000" "CrossDissolve000000" ] [ "sourceGroup000001" "CrossDissolve000000" ] [ "sourceGroup000003" "CrossDissolve000001" ] [ "sourceGroup000004" "CrossDissolve000001" ] [ "CrossDissolve000000" "Sequence000000" ] [ "sourceGroup000002" "Sequence000000" ] [ "CrossDissolve000001" "Sequence000000" ] [ "sourceGroup000005" "Sequence000000" ] [ "sourceGroup000006" "Sequence000000" ] [ "CrossDissolve000002" "Sequence000000" ] ] - } -} - -CrossDissolve000000 : CrossDissolve (1) -{ - output - { - float fps = 24 - } - - parameters - { - float numFrames = 20 - float startFrame = 1 - } - - ui - { - string name = "t0" - } -} - -CrossDissolve000001 : CrossDissolve (1) -{ - output - { - float fps = 24 - } - - parameters - { - float numFrames = 20 - float startFrame = 1 - } - - ui - { - string name = "t1" - } -} - -CrossDissolve000002 : CrossDissolve (1) -{ - output - { - float fps = 24 - } - - parameters - { - float numFrames = 20 - float startFrame = 1 - } - - ui - { - string name = "t3" - } -} - -Sequence000000 : RVSequenceGroup (1) -{ - ui - { - string name = "Sequence1" - } -} - -Stack000000 : RVStackGroup (1) -{ - ui - { - string name = "tracks" - } -} - -sourceGroup000000 : RVSourceGroup (1) -{ - ui - { - string name = "_transition_pre" - } -} - -sourceGroup000000_source : RVFileSource (1) -{ - cut - { - int in = 0 - int out = 19 - } - - group - { - float fps = 24 - } - - media - { - string movie = "blank,start=0.0,end=19.0,fps=24.0.movieproc" - } -} - -sourceGroup000001 : RVSourceGroup (1) -{ - ui - { - string name = "A_transition_post" - } -} - -sourceGroup000001_source : RVFileSource (1) -{ - cut - { - int in = -10 - int out = 9 - } - - group - { - float fps = 24 - } - - media - { - string movie = "smptebars,start=-10.0,end=9.0,fps=24.0.movieproc" - } -} - -sourceGroup000002 : RVSourceGroup (1) -{ - ui - { - string name = "A" - } -} - -sourceGroup000002_source : RVFileSource (1) -{ - cut - { - int in = 10 - int out = 39 - } - - group - { - float fps = 24 - } - - media - { - string movie = "smptebars,start=10.0,end=39.0,fps=24.0.movieproc" - } -} - -sourceGroup000003 : RVSourceGroup (1) -{ - ui - { - string name = "A_transition_pre" - } -} - -sourceGroup000003_source : RVFileSource (1) -{ - cut - { - int in = 40 - int out = 59 - } - - group - { - float fps = 24 - } - - media - { - string movie = "smptebars,start=40.0,end=59.0,fps=24.0.movieproc" - } -} - -sourceGroup000004 : RVSourceGroup (1) -{ - ui - { - string name = "B_transition_post" - } -} - -sourceGroup000004_source : RVFileSource (1) -{ - cut - { - int in = -10 - int out = 9 - } - - group - { - float fps = 24 - } - - media - { - string movie = "smptebars,start=-10.0,end=9.0,fps=24.0.movieproc" - } -} - -sourceGroup000005 : RVSourceGroup (1) -{ - ui - { - string name = "B" - } -} - -sourceGroup000005_source : RVFileSource (1) -{ - cut - { - int in = 10 - int out = 49 - } - - group - { - float fps = 24 - } - - media - { - string movie = "smptebars,start=10.0,end=49.0,fps=24.0.movieproc" - } -} - -sourceGroup000006 : RVSourceGroup (1) -{ - ui - { - string name = "C" - } -} - -sourceGroup000006_source : RVFileSource (1) -{ - cut - { - int in = 0 - int out = 39 - } - - group - { - float fps = 24 - } - - media - { - string movie = "smptebars,start=0.0,end=39.0,fps=24.0.movieproc" - } -} - -sourceGroup000007 : RVSourceGroup (1) -{ - ui - { - string name = "C_transition_pre" - } -} - -sourceGroup000007_source : RVFileSource (1) -{ - cut - { - int in = 40 - int out = 59 - } - - group - { - float fps = 24 - } - - media - { - string movie = "smptebars,start=40.0,end=59.0,fps=24.0.movieproc" - } -} - -sourceGroup000008 : RVSourceGroup (1) -{ - ui - { - string name = "_transition_post" - } -} - -sourceGroup000008_source : RVFileSource (1) -{ - cut - { - int in = -10 - int out = 9 - } - - group - { - float fps = 24 - } - - media - { - string movie = "blank,start=-10.0,end=9.0,fps=24.0.movieproc" - } -} diff --git a/contrib/opentimelineio_contrib/adapters/tests/test_aaf_adapter.py b/contrib/opentimelineio_contrib/adapters/tests/test_aaf_adapter.py index 8c202f91b..88ec22c52 100644 --- a/contrib/opentimelineio_contrib/adapters/tests/test_aaf_adapter.py +++ b/contrib/opentimelineio_contrib/adapters/tests/test_aaf_adapter.py @@ -1,27 +1,5 @@ -# -*- coding: utf-8 -*- -# +# SPDX-License-Identifier: Apache-2.0 # Copyright Contributors to the OpenTimelineIO project -# -# Licensed under the Apache License, Version 2.0 (the "Apache License") -# with the following modification; you may not use this file except in -# compliance with the Apache License and the following modification to it: -# Section 6. Trademarks. is deleted and replaced with: -# -# 6. Trademarks. This License does not grant permission to use the trade -# names, trademarks, service marks, or product names of the Licensor -# and its affiliates, except as required to comply with Section 4(c) of -# the License and to reproduce the content of the NOTICE file. -# -# You may obtain a copy of the Apache License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the Apache License with the above modification is -# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -# KIND, either express or implied. See the Apache License for the specific -# language governing permissions and limitations under the Apache License. -# """Test the AAF adapter.""" @@ -37,12 +15,7 @@ AAFValidationError ) -try: - # python2 - import StringIO as io -except ImportError: - # python3 - import io +import io TRANSCRIPTION_RESULT = """--- @@ -226,16 +199,6 @@ ) -def safe_str(maybe_str): - """To help with testing between python 2 and 3, this function attempts to - decode a string, and if it cannot decode it just returns the string. - """ - try: - return maybe_str.decode('utf-8') - except AttributeError: - return maybe_str - - try: lib_path = os.environ.get("OTIO_AAF_PYTHON_LIB") if lib_path and lib_path not in sys.path: @@ -279,7 +242,7 @@ def test_aaf_read(self): self.assertEqual(len(timeline.audio_tracks()), 2) - clips = list(video_track.each_clip()) + clips = video_track.find_clips() self.assertEqual( [ @@ -359,7 +322,7 @@ def test_aaf_read_trims(self): ] ) - clips = list(video_track.each_clip()) + clips = video_track.find_clips() self.assertEqual( [item.name for item in video_track], @@ -463,7 +426,7 @@ def test_aaf_read_transitions(self): video_track = video_tracks[0] self.assertEqual(len(video_track), 12) - clips = list(video_track.each_clip()) + clips = video_track.find_clips() self.assertEqual(len(clips), 4) self.assertEqual( @@ -621,7 +584,7 @@ def test_aaf_user_comments(self): correctWords = [ "test1", "testing 1 2 3", - u"Eyjafjallaj\xf6kull", + "Eyjafjallaj\xf6kull", "'s' \"d\" `b`", None, # Gap None @@ -668,7 +631,7 @@ def test_aaf_flatten_tracks(self): t.name = "" t.metadata.pop("AAF", None) - for c in t.each_child(): + for c in t.find_children(): if hasattr(c, "media_reference") and c.media_reference: mr = c.media_reference mr.metadata.get("AAF", {}).pop('LastModified', None) @@ -679,7 +642,7 @@ def test_aaf_flatten_tracks(self): meta.pop('StartTime', None) # We don't care about Gap start times, only their duration matters - for g in t.each_child(descended_from_type=otio.schema.Gap): + for g in t.find_children(descended_from_type=otio.schema.Gap): dur = g.source_range.duration rate = g.source_range.start_time.rate g.source_range = otio.opentime.TimeRange( @@ -903,20 +866,20 @@ def test_2997fps(self): def test_utf8_names(self): timeline = otio.adapters.read_from_file(UTF8_CLIP_PATH) self.assertEqual( - (u"Sequence_ABCXYZñç꜕∑´®†¥¨ˆøπ“‘åß∂ƒ©˙∆˚¬…æΩ≈ç√∫˜µ≤≥÷.Exported.01"), - safe_str(timeline.name) + ("Sequence_ABCXYZñç꜕∑´®†¥¨ˆøπ“‘åß∂ƒ©˙∆˚¬…æΩ≈ç√∫˜µ≤≥÷.Exported.01"), + timeline.name ) video_track = timeline.video_tracks()[0] first_clip = video_track[0] self.assertEqual( - safe_str(first_clip.name), - (u"Clip_ABCXYZñç꜕∑´®†¥¨ˆøπ“‘åß∂ƒ©˙∆˚¬…æΩ≈ç√∫˜µ≤≥÷") + first_clip.name, + ("Clip_ABCXYZñç꜕∑´®†¥¨ˆøπ“‘åß∂ƒ©˙∆˚¬…æΩ≈ç√∫˜µ≤≥÷") ) self.assertEqual( ( first_clip.media_reference.metadata["AAF"]["UserComments"]["Comments"] ).encode('utf-8'), - (u"Comments_ABCXYZñç꜕∑´®†¥¨ˆøπ“‘åß∂ƒ©˙∆˚¬…æΩ≈ç√∫˜µ≤≥÷").encode('utf-8') + ("Comments_ABCXYZñç꜕∑´®†¥¨ˆøπ“‘åß∂ƒ©˙∆˚¬…æΩ≈ç√∫˜µ≤≥÷").encode() ) def test_multiple_top_level_mobs(self): @@ -1002,7 +965,7 @@ def test_aaf_sourcemob_usage(self): # `Usage_SubClip` value subclip_timeline = otio.adapters.read_from_file(SUBCLIP_PATH) subclip_usages = {"Subclip.BREATH": "Usage_SubClip"} - for clip in subclip_timeline.each_clip(): + for clip in subclip_timeline.find_clips(): self.assertEqual( clip.metadata.get("AAF", {}).get("SourceMobUsage"), subclip_usages[clip.name] @@ -1017,7 +980,7 @@ def test_aaf_sourcemob_usage(self): "t-hawk (loop)-HD.mp4": "", "tech.fux (loop)-HD.mp4": "" } - for clip in simple_timeline.each_clip(): + for clip in simple_timeline.find_clips(): self.assertEqual( clip.metadata.get("AAF", {}).get("SourceMobUsage", ""), simple_usages[clip.name] @@ -1238,9 +1201,9 @@ def test_attach_markers(self): all_markers = {} for i, track in enumerate( - timeline.each_child(descended_from_type=otio.schema.Track) + timeline.find_children(descended_from_type=otio.schema.Track) ): - for item in track.each_child(): + for item in track.find_children(): markers = [ ( m.name, @@ -1257,7 +1220,7 @@ def test_attach_markers(self): def test_keyframed_properties(self): def get_expected_dict(timeline): expected = [] - for clip in timeline.each_child(descended_from_type=otio.schema.Clip): + for clip in timeline.find_children(descended_from_type=otio.schema.Clip): for effect in clip.effects: props = {} parameters = effect.metadata.get("AAF", {}).get("Parameters", {}) @@ -1603,7 +1566,7 @@ def test_aaf_writer_nometadata(self): def _target_url_fixup(timeline): # fixes up relative paths to be absolute to this test file test_dir = os.path.dirname(os.path.abspath(__file__)) - for clip in timeline.each_clip(): + for clip in timeline.find_clips(): target_url_str = clip.media_reference.target_url clip.media_reference.target_url = os.path.join(test_dir, target_url_str) @@ -1644,7 +1607,7 @@ def test_aaf_roundtrip_first_clip(self): def _target_url_fixup(timeline): # fixes up relative paths to be absolute to this test file test_dir = os.path.dirname(os.path.abspath(__file__)) - for clip in timeline.each_clip(): + for clip in timeline.find_clips(): target_url_str = clip.media_reference.target_url clip.media_reference.target_url = os.path.join(test_dir, target_url_str) @@ -1658,8 +1621,8 @@ def _target_url_fixup(timeline): def _verify_first_clip(self, original_timeline, aaf_path): timeline_from_aaf = otio.adapters.read_from_file(aaf_path) - original_clips = list(original_timeline.each_clip()) - aaf_clips = list(timeline_from_aaf.each_clip()) + original_clips = original_timeline.find_clips() + aaf_clips = timeline_from_aaf.find_clips() self.assertTrue(len(original_clips) > 0) self.assertEqual(len(aaf_clips), len(original_clips)) @@ -1671,12 +1634,12 @@ def _verify_first_clip(self, original_timeline, aaf_path): for prop in ['source_range']: self.assertEqual(getattr(first_clip_in_original_timeline, prop), getattr(first_clip_in_aaf_timeline, prop), - "`{}` did not match".format(prop)) + f"`{prop}` did not match") for method in ['visible_range', 'trimmed_range']: self.assertEqual(getattr(first_clip_in_original_timeline, method)(), getattr(first_clip_in_aaf_timeline, method)(), - "`{}` did not match".format(method)) + f"`{method}` did not match") def test_aaf_writer_nesting(self): self._verify_aaf(NESTING_EXAMPLE_PATH) @@ -1754,10 +1717,11 @@ def _verify_aaf(self, aaf_path): sequence = opgroup.segments[0] self.assertTrue(isinstance(sequence, Sequence)) - self.assertEqual(len(list(otio_track.each_child(shallow_search=True))), - len(sequence.components)) + self.assertEqual( + len(otio_track.find_children(shallow_search=True)), + len(sequence.components)) for otio_child, aaf_component in zip( - otio_track.each_child(shallow_search=True), + otio_track.find_children(shallow_search=True), sequence.components): type_mapping = { otio.schema.Clip: aaf2.components.SourceClip, @@ -1775,7 +1739,7 @@ def _verify_aaf(self, aaf_path): if isinstance(aaf_component, aaf2.components.OperationGroup): nested_aaf_segments = aaf_component.segments for nested_otio_child, nested_aaf_segment in zip( - otio_child.each_child(), nested_aaf_segments): + otio_child.find_children(), nested_aaf_segments): self._is_otio_aaf_same(nested_otio_child, nested_aaf_segment) else: diff --git a/contrib/opentimelineio_contrib/adapters/tests/test_ale_adapter.py b/contrib/opentimelineio_contrib/adapters/tests/test_ale_adapter.py index 8c0a96ee4..089ad1635 100644 --- a/contrib/opentimelineio_contrib/adapters/tests/test_ale_adapter.py +++ b/contrib/opentimelineio_contrib/adapters/tests/test_ale_adapter.py @@ -1,26 +1,5 @@ -# +# SPDX-License-Identifier: Apache-2.0 # Copyright Contributors to the OpenTimelineIO project -# -# Licensed under the Apache License, Version 2.0 (the "Apache License") -# with the following modification; you may not use this file except in -# compliance with the Apache License and the following modification to it: -# Section 6. Trademarks. is deleted and replaced with: -# -# 6. Trademarks. This License does not grant permission to use the trade -# names, trademarks, service marks, or product names of the Licensor -# and its affiliates, except as required to comply with Section 4(c) of -# the License and to reproduce the content of the NOTICE file. -# -# You may obtain a copy of the Apache License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the Apache License with the above modification is -# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -# KIND, either express or implied. See the Apache License for the specific -# language governing permissions and limitations under the Apache License. -# """Test the ALE adapter.""" @@ -229,7 +208,7 @@ def add_then_check(timeline, size, expected_format): def test_ale_roundtrip(self): ale_path = EXAMPLE_PATH - with open(ale_path, 'r') as fi: + with open(ale_path) as fi: original = fi.read() collection = otio.adapters.read_from_string(original, "ale") output = otio.adapters.write_to_string(collection, "ale") diff --git a/contrib/opentimelineio_contrib/adapters/tests/test_burnins.py b/contrib/opentimelineio_contrib/adapters/tests/test_burnins.py index 7257ded1d..622ec5a11 100644 --- a/contrib/opentimelineio_contrib/adapters/tests/test_burnins.py +++ b/contrib/opentimelineio_contrib/adapters/tests/test_burnins.py @@ -1,26 +1,6 @@ -# +# SPDX-License-Identifier: Apache-2.0 # Copyright Contributors to the OpenTimelineIO project -# -# Licensed under the Apache License, Version 2.0 (the "Apache License") -# with the following modification; you may not use this file except in -# compliance with the Apache License and the following modification to it: -# Section 6. Trademarks. is deleted and replaced with: -# -# 6. Trademarks. This License does not grant permission to use the trade -# names, trademarks, service marks, or product names of the Licensor -# and its affiliates, except as required to comply with Section 4(c) of -# the License and to reproduce the content of the NOTICE file. -# -# You may obtain a copy of the Apache License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the Apache License with the above modification is -# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -# KIND, either express or implied. See the Apache License for the specific -# language governing permissions and limitations under the Apache License. -# + """Unit tests for the rv session file adapter""" import unittest diff --git a/contrib/opentimelineio_contrib/adapters/tests/test_fcpx_adapter.py b/contrib/opentimelineio_contrib/adapters/tests/test_fcpx_adapter.py index 70370eae8..8eafbb58e 100644 --- a/contrib/opentimelineio_contrib/adapters/tests/test_fcpx_adapter.py +++ b/contrib/opentimelineio_contrib/adapters/tests/test_fcpx_adapter.py @@ -1,24 +1,15 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright Contributors to the OpenTimelineIO project + import os import subprocess import sys import unittest +import unittest.mock import opentimelineio as otio import opentimelineio.test_utils as otio_test_utils from opentimelineio_contrib.adapters.fcpx_xml import format_name -try: - # Python 3.3 forward includes the mock module - from unittest import mock - could_import_mock = True -except ImportError: - # Fallback for older python (not included in standard library) - try: - import mock - could_import_mock = True - except ImportError: - # Mock appears to not be installed - could_import_mock = False - SAMPLE_LIBRARY_XML = os.path.join( os.path.dirname(__file__), "sample_data", @@ -47,14 +38,13 @@ class AdaptersFcpXXmlTest(unittest.TestCase, otio_test_utils.OTIOAssertions): """ def __init__(self, *args, **kwargs): - super(AdaptersFcpXXmlTest, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) self.maxDiff = None def test_library_roundtrip(self): container = otio.adapters.read_from_file(SAMPLE_LIBRARY_XML) - timeline = next( - container.each_child(descended_from_type=otio.schema.Timeline) - ) + timeline = container.find_children( + descended_from_type=otio.schema.Timeline)[0] self.assertIsNotNone(timeline) self.assertEqual(len(timeline.tracks), 4) @@ -90,9 +80,8 @@ def test_library_roundtrip(self): def test_event_roundtrip(self): container = otio.adapters.read_from_file(SAMPLE_EVENT_XML) - timeline = next( - container.each_child(descended_from_type=otio.schema.Timeline) - ) + timeline = container.find_children( + descended_from_type=otio.schema.Timeline)[0] self.assertIsNotNone(timeline) self.assertEqual(len(timeline.tracks), 4) @@ -169,16 +158,17 @@ def test_clips_roundtrip(self): new_timeline = otio.adapters.read_from_string(fcpx_xml, "fcpx_xml") self.assertJsonEqual(container, new_timeline) - @unittest.skipIf( - not could_import_mock, - "mock module not found. Install mock from pypi or use python >= 3.3." - ) def test_format_name(self): - rvalue = subprocess.check_output([sys.executable, '-c', 'print("640x360")']) - with mock.patch.object(subprocess, 'check_output', return_value=rvalue): - with mock.patch.object(os.path, 'exists', return_value=True): - self.assertEqual(format_name( - 25, "file:///dummy.me"), 'FFVideoFormat640x360p25') + rvalue = subprocess.check_output( + [sys.executable, '-c', 'print("640x360")'] + ) + mock_patch = unittest.mock.patch.object + with mock_patch(subprocess, 'check_output', return_value=rvalue): + with mock_patch(os.path, 'exists', return_value=True): + self.assertEqual( + format_name(25, "file:///dummy.me"), + 'FFVideoFormat640x360p25' + ) if __name__ == '__main__': diff --git a/contrib/opentimelineio_contrib/adapters/tests/test_hls_playlist_adapter.py b/contrib/opentimelineio_contrib/adapters/tests/test_hls_playlist_adapter.py index 4b2cca05c..cac54afb0 100644 --- a/contrib/opentimelineio_contrib/adapters/tests/test_hls_playlist_adapter.py +++ b/contrib/opentimelineio_contrib/adapters/tests/test_hls_playlist_adapter.py @@ -1,40 +1,12 @@ -# +# SPDX-License-Identifier: Apache-2.0 # Copyright Contributors to the OpenTimelineIO project -# -# Licensed under the Apache License, Version 2.0 (the "Apache License") -# with the following modification; you may not use this file except in -# compliance with the Apache License and the following modification to it: -# Section 6. Trademarks. is deleted and replaced with: -# -# 6. Trademarks. This License does not grant permission to use the trade -# names, trademarks, service marks, or product names of the Licensor -# and its affiliates, except as required to comply with Section 4(c) of -# the License and to reproduce the content of the NOTICE file. -# -# You may obtain a copy of the Apache License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the Apache License with the above modification is -# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -# KIND, either express or implied. See the Apache License for the specific -# language governing permissions and limitations under the Apache License. -# import os import unittest import opentimelineio as otio - -# handle python2 vs python3 difference -try: - from tempfile import TemporaryDirectory # noqa: F401 - import tempfile -except ImportError: - # XXX: python2.7 only - from backports import tempfile +import tempfile # Reference data SAMPLE_DATA_DIR = os.path.join(os.path.dirname(__file__), "sample_data") @@ -348,7 +320,7 @@ def test_iframe_segment_size(self): # Configure the playlist to be an iframe list track_hls_metadata = timeline.tracks[0].metadata['HLS'] - del(track_hls_metadata['EXT-X-INDEPENDENT-SEGMENTS']) + del track_hls_metadata['EXT-X-INDEPENDENT-SEGMENTS'] track_hls_metadata['EXT-X-I-FRAMES-ONLY'] = None # Write out the playlist @@ -447,7 +419,7 @@ def test_simple_master_pl_from_mem(self): pl_string = f.read() # Drop blank lines before comparing - pl_string = '\n'.join((line for line in pl_string.split('\n') if line)) + pl_string = '\n'.join(line for line in pl_string.split('\n') if line) # Compare against the reference value self.assertEqual(pl_string, MEM_MASTER_PLAYLIST_REF_VALUE) @@ -719,7 +691,7 @@ def test_explicit_master_pl_from_mem(self): pl_string = f.read() # Drop blank lines before comparing - pl_string = '\n'.join((line for line in pl_string.split('\n') if line)) + pl_string = '\n'.join(line for line in pl_string.split('\n') if line) # Compare against the reference value self.assertEqual(pl_string, MEM_SINGLE_TRACK_MASTER_PLAYLIST_REF_VALUE) diff --git a/contrib/opentimelineio_contrib/adapters/tests/test_kdenlive_adapter.py b/contrib/opentimelineio_contrib/adapters/tests/test_kdenlive_adapter.py deleted file mode 100644 index 44a992dcd..000000000 --- a/contrib/opentimelineio_contrib/adapters/tests/test_kdenlive_adapter.py +++ /dev/null @@ -1,66 +0,0 @@ -import unittest -import opentimelineio as otio -import opentimelineio.test_utils as otio_test_utils -import os - - -class AdaptersKdenliveTest(unittest.TestCase, otio_test_utils.OTIOAssertions): - - def __init__(self, *args, **kwargs): - super(AdaptersKdenliveTest, self).__init__(*args, **kwargs) - - def test_library_roundtrip(self): - timeline = otio.adapters.read_from_file( - os.path.join(os.path.dirname(__file__), "sample_data", - "kdenlive_example.kdenlive")) - - self.assertIsNotNone(timeline) - self.assertEqual(len(timeline.tracks), 5) - - self.assertEqual(len(timeline.video_tracks()), 2) - self.assertEqual(len(timeline.audio_tracks()), 3) - - clip_urls = (('AUD0002.OGG',), - ('AUD0001.OGG', 'AUD0001.OGG'), - ('VID0001.MKV', 'VID0001.MKV'), - ('VID0001.MKV', 'VID0001.MKV'), - ('VID0002.MKV', 'VID0003.MKV')) - - for n, track in enumerate(timeline.tracks): - self.assertTupleEqual( - clip_urls[n], - tuple(c.media_reference.target_url - for c in track - if isinstance(c, otio.schema.Clip) and - isinstance( - c.media_reference, - otio.schema.ExternalReference))) - - kdenlive_xml = otio.adapters.write_to_string(timeline, "kdenlive") - self.assertIsNotNone(kdenlive_xml) - - new_timeline = otio.adapters.read_from_string(kdenlive_xml, "kdenlive") - self.assertJsonEqual(timeline, new_timeline) - - def test_from_fcp_example(self): - timeline = otio.adapters.read_from_file( - os.path.join( - os.path.dirname(__file__), - "sample_data", - "kdenlive_example_from_fcp.xml", - ), - ) - - kdenlive_xml = otio.adapters.write_to_string(timeline, "kdenlive") - self.assertIsNotNone(kdenlive_xml) - - new_timeline = otio.adapters.read_from_string(kdenlive_xml, "kdenlive") - troublesome_clip = new_timeline.video_tracks()[0][35] - self.assertEqual( - troublesome_clip.source_range.duration.value, - 807, - ) - - -if __name__ == '__main__': - unittest.main() diff --git a/contrib/opentimelineio_contrib/adapters/tests/test_maya_sequencer.py b/contrib/opentimelineio_contrib/adapters/tests/test_maya_sequencer.py index c8fb9ba24..d0414d781 100644 --- a/contrib/opentimelineio_contrib/adapters/tests/test_maya_sequencer.py +++ b/contrib/opentimelineio_contrib/adapters/tests/test_maya_sequencer.py @@ -1,26 +1,5 @@ -# +# SPDX-License-Identifier: Apache-2.0 # Copyright Contributors to the OpenTimelineIO project -# -# Licensed under the Apache License, Version 2.0 (the "Apache License") -# with the following modification; you may not use this file except in -# compliance with the Apache License and the following modification to it: -# Section 6. Trademarks. is deleted and replaced with: -# -# 6. Trademarks. This License does not grant permission to use the trade -# names, trademarks, service marks, or product names of the Licensor -# and its affiliates, except as required to comply with Section 4(c) of -# the License and to reproduce the content of the NOTICE file. -# -# You may obtain a copy of the Apache License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the Apache License with the above modification is -# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -# KIND, either express or implied. See the Apache License for the specific -# language governing permissions and limitations under the Apache License. -# """Unit tests for the maya sequencer adapter""" diff --git a/contrib/opentimelineio_contrib/adapters/tests/test_rvsession.py b/contrib/opentimelineio_contrib/adapters/tests/test_rvsession.py deleted file mode 100644 index 4c6eae6ab..000000000 --- a/contrib/opentimelineio_contrib/adapters/tests/test_rvsession.py +++ /dev/null @@ -1,612 +0,0 @@ -# -# Copyright Contributors to the OpenTimelineIO project -# -# Licensed under the Apache License, Version 2.0 (the "Apache License") -# with the following modification; you may not use this file except in -# compliance with the Apache License and the following modification to it: -# Section 6. Trademarks. is deleted and replaced with: -# -# 6. Trademarks. This License does not grant permission to use the trade -# names, trademarks, service marks, or product names of the Licensor -# and its affiliates, except as required to comply with Section 4(c) of -# the License and to reproduce the content of the NOTICE file. -# -# You may obtain a copy of the Apache License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the Apache License with the above modification is -# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -# KIND, either express or implied. See the Apache License for the specific -# language governing permissions and limitations under the Apache License. -# - -"""Unit tests for the rv session file adapter""" - -import os -import tempfile -import unittest - -import opentimelineio as otio - -SAMPLE_DATA_DIR = os.path.join(os.path.dirname(__file__), "sample_data") -SCREENING_EXAMPLE_PATH = os.path.join(SAMPLE_DATA_DIR, "screening_example.edl") -TRANSITION_EXAMPLE_PATH = os.path.join(SAMPLE_DATA_DIR, "transition_test.otio") -BASELINE_PATH = os.path.join(SAMPLE_DATA_DIR, "screening_example.rv") -BASELINE_TRANSITION_PATH = os.path.join(SAMPLE_DATA_DIR, "transition_test.rv") -METADATA_EXAMPLE_PATH = os.path.join(SAMPLE_DATA_DIR, "rv_metadata.otio") -METADATA_BASELINE_PATH = os.path.join(SAMPLE_DATA_DIR, "rv_metadata.rv") -IMAGE_SEQUENCE_EXAMPLE_PATH = os.path.join( - SAMPLE_DATA_DIR, - "image_sequence_example.otio" -) - - -SAMPLE_DATA = """{ - "OTIO_SCHEMA": "Timeline.1", - "tracks": { - "OTIO_SCHEMA": "Stack.1", - "children": [{ - "OTIO_SCHEMA": "Track.1", - "kind": "Video", - "children": [{ - "OTIO_SCHEMA": "Gap.1", - "source_range": { - "OTIO_SCHEMA": "TimeRange.1", - "duration": { - "OTIO_SCHEMA": "RationalTime.1", - "rate": 24.0, "value": 10.0 - }, - "start_time": { - "OTIO_SCHEMA": "RationalTime.1", - "rate": 24.0, "value": 0.0 - } - } - }, - { - "OTIO_SCHEMA": "Transition.1", - "transition_type": "SMPTE_Dissolve", - "in_offset": { - "OTIO_SCHEMA": "RationalTime.1", - "rate": 24.0, "value": 10.0 - }, - "out_offset": { - "OTIO_SCHEMA": "RationalTime.1", - "rate": 24.0, "value": 10.0 - } - }, - { - "OTIO_SCHEMA": "Clip.1", - "media_reference": { - "OTIO_SCHEMA": "MissingReference.1" - }, - "source_range": { - "OTIO_SCHEMA": "TimeRange.1", - "duration": { - "OTIO_SCHEMA": "RationalTime.1", - "rate": 24.0, "value": 10.0 - }, - "start_time": { - "OTIO_SCHEMA": "RationalTime.1", - "rate": 24.0, "value": 10.0 - } - } - }] - }] - } -}""" - - -AUDIO_VIDEO_SAMPLE_DATA = """{ - "OTIO_SCHEMA": "Timeline.1", - "metadata": {}, - "name": null, - "tracks": { - "OTIO_SCHEMA": "Stack.1", - "children": [ - { - "OTIO_SCHEMA": "Track.1", - "children": [ - { - "OTIO_SCHEMA": "Clip.1", - "effects": [], - "markers": [], - "media_reference": { - "OTIO_SCHEMA": "ExternalReference.1", - "available_range": { - "OTIO_SCHEMA": "TimeRange.1", - "duration": { - "OTIO_SCHEMA": "RationalTime.1", - "rate": 25, - "value": 67 - }, - "start_time": { - "OTIO_SCHEMA": "RationalTime.1", - "rate": 25, - "value": 0 - } - }, - "metadata": {}, - "name": null, - "target_url": "/path/to/video.mov" - }, - "metadata": {}, - "name": "plyblast", - "source_range": { - "OTIO_SCHEMA": "TimeRange.1", - "duration": { - "OTIO_SCHEMA": "RationalTime.1", - "rate": 25, - "value": 67 - }, - "start_time": { - "OTIO_SCHEMA": "RationalTime.1", - "rate": 25, - "value": 54 - } - } - } - ], - "effects": [], - "kind": "Video", - "markers": [], - "metadata": {}, - "name": "v1", - "source_range": null - }, - { - "OTIO_SCHEMA": "Track.1", - "children": [ - { - "OTIO_SCHEMA": "Clip.1", - "effects": [], - "markers": [], - "media_reference": { - "OTIO_SCHEMA": "ExternalReference.1", - "available_range": { - "OTIO_SCHEMA": "TimeRange.1", - "duration": { - "OTIO_SCHEMA": "RationalTime.1", - "rate": 25, - "value": 500 - }, - "start_time": { - "OTIO_SCHEMA": "RationalTime.1", - "rate": 25, - "value": 0 - } - }, - "metadata": {}, - "name": null, - "target_url": "/path/to/audio.wav" - }, - "metadata": {}, - "name": "sound", - "source_range": { - "OTIO_SCHEMA": "TimeRange.1", - "duration": { - "OTIO_SCHEMA": "RationalTime.1", - "rate": 25, - "value": 67 - }, - "start_time": { - "OTIO_SCHEMA": "RationalTime.1", - "rate": 25, - "value": 54 - } - } - } - ], - "effects": [], - "kind": "Audio", - "markers": [], - "metadata": {}, - "name": "a1", - "source_range": null - } - ], - "effects": [], - "markers": [], - "metadata": {}, - "name": "tracks", - "source_range": null - } -}""" - - -NESTED_STACK_SAMPLE_DATA = """{ - "OTIO_SCHEMA": "Timeline.1", - "metadata": {}, - "name": "My Timeline", - "tracks": { - "OTIO_SCHEMA": "Stack.1", - "children": [ - { - "OTIO_SCHEMA": "Track.1", - "children": [ - { - "OTIO_SCHEMA": "Clip.1", - "effects": [], - "markers": [], - "media_reference": { - "OTIO_SCHEMA": "ExternalReference.1", - "available_range": { - "OTIO_SCHEMA": "TimeRange.1", - "duration": { - "OTIO_SCHEMA": "RationalTime.1", - "rate": 24, - "value": 238 - }, - "start_time": { - "OTIO_SCHEMA": "RationalTime.1", - "rate": 24, - "value": 0 - } - }, - "metadata": {}, - "target_url": "/path/to/some/video.mov" - }, - "metadata": {}, - "name": "Normal Clip 1", - "source_range": { - "OTIO_SCHEMA": "TimeRange.1", - "duration": { - "OTIO_SCHEMA": "RationalTime.1", - "rate": 24, - "value": 238 - }, - "start_time": { - "OTIO_SCHEMA": "RationalTime.1", - "rate": 24, - "value": 135 - } - } - }, - { - "OTIO_SCHEMA": "Stack.1", - "children": [ - { - "OTIO_SCHEMA": "Clip.1", - "effects": [], - "markers": [], - "media_reference": { - "OTIO_SCHEMA": "ExternalReference.1", - "available_range": { - "OTIO_SCHEMA": "TimeRange.1", - "duration": { - "OTIO_SCHEMA": "RationalTime.1", - "rate": 24, - "value": 238 - }, - "start_time": { - "OTIO_SCHEMA": "RationalTime.1", - "rate": 24, - "value": 0 - } - }, - "metadata": {}, - "target_url": "/path/to/some/video.mov" - }, - "metadata": {}, - "name": "Clip Inside A Stack 1", - "source_range": { - "OTIO_SCHEMA": "TimeRange.1", - "duration": { - "OTIO_SCHEMA": "RationalTime.1", - "rate": 24, - "value": 37 - }, - "start_time": { - "OTIO_SCHEMA": "RationalTime.1", - "rate": 24, - "value": 373 - } - } - } - ], - "effects": [], - "markers": [], - "metadata": {}, - "name": "Nested Stack 1", - "source_range": { - "OTIO_SCHEMA": "TimeRange.1", - "duration": { - "OTIO_SCHEMA": "RationalTime.1", - "rate": 24, - "value": 31 - }, - "start_time": { - "OTIO_SCHEMA": "RationalTime.1", - "rate": 24, - "value": 0 - } - } - } - ], - "effects": [], - "kind": "Video", - "markers": [], - "metadata": {}, - "name": "Top Level Track", - "source_range": null - }, - { - "OTIO_SCHEMA": "Track.1", - "children": [ - { - "OTIO_SCHEMA": "Clip.1", - "effects": [], - "markers": [], - "media_reference": { - "OTIO_SCHEMA": "ExternalReference.1", - "available_range": { - "OTIO_SCHEMA": "TimeRange.1", - "duration": { - "OTIO_SCHEMA": "RationalTime.1", - "rate": 24, - "value": 238 - }, - "start_time": { - "OTIO_SCHEMA": "RationalTime.1", - "rate": 24, - "value": 0 - } - }, - "metadata": {}, - "target_url": "/path/to/some/audio.wav" - }, - "metadata": {}, - "name": "Normal Clip 1", - "source_range": { - "OTIO_SCHEMA": "TimeRange.1", - "duration": { - "OTIO_SCHEMA": "RationalTime.1", - "rate": 24, - "value": 238 - }, - "start_time": { - "OTIO_SCHEMA": "RationalTime.1", - "rate": 24, - "value": 135 - } - } - }, - { - "OTIO_SCHEMA": "Stack.1", - "children": [ - { - "OTIO_SCHEMA": "Clip.1", - "effects": [], - "markers": [], - "media_reference": { - "OTIO_SCHEMA": "ExternalReference.1", - "available_range": { - "OTIO_SCHEMA": "TimeRange.1", - "duration": { - "OTIO_SCHEMA": "RationalTime.1", - "rate": 24, - "value": 238 - }, - "start_time": { - "OTIO_SCHEMA": "RationalTime.1", - "rate": 24, - "value": 0 - } - }, - "metadata": {}, - "target_url": "/path/to/some/audio.wav" - }, - "metadata": {}, - "name": "Clip Inside A Stack 1", - "source_range": { - "OTIO_SCHEMA": "TimeRange.1", - "duration": { - "OTIO_SCHEMA": "RationalTime.1", - "rate": 24, - "value": 37 - }, - "start_time": { - "OTIO_SCHEMA": "RationalTime.1", - "rate": 24, - "value": 373 - } - } - } - ], - "effects": [], - "markers": [], - "metadata": {}, - "name": "Nested Stack 1", - "source_range": { - "OTIO_SCHEMA": "TimeRange.1", - "duration": { - "OTIO_SCHEMA": "RationalTime.1", - "rate": 24, - "value": 31 - }, - "start_time": { - "OTIO_SCHEMA": "RationalTime.1", - "rate": 24, - "value": 0 - } - } - } - ], - "effects": [], - "kind": "Audio", - "markers": [], - "metadata": {}, - "name": "Top Level Track", - "source_range": null - } - ], - "effects": [], - "markers": [], - "metadata": {}, - "name": "Top Level Stack", - "source_range": null - } -}""" - - -@unittest.skipIf( - "OTIO_RV_PYTHON_LIB" not in os.environ or - "OTIO_RV_PYTHON_BIN" not in os.environ, - "OTIO_RV_PYTHON_BIN or OTIO_RV_PYTHON_LIB not set." -) -class RVSessionAdapterReadTest(unittest.TestCase): - def setUp(self): - fd, self.tmp_path = tempfile.mkstemp(suffix=".rv", text=True) - - # Close file descriptor to avoid leak. We only need the tmp_path. - os.close(fd) - - def tearDown(self): - os.unlink(self.tmp_path) - - def test_basic_rvsession_read(self): - timeline = otio.adapters.read_from_file(SCREENING_EXAMPLE_PATH) - - otio.adapters.write_to_file(timeline, self.tmp_path) - - with open(self.tmp_path) as fo: - test_data = fo.read() - - with open(BASELINE_PATH) as fo: - baseline_data = fo.read() - - self.maxDiff = None - self._connectionFreeAssertMultiLineEqual(baseline_data, test_data) - - def test_transition_rvsession_read(self): - timeline = otio.adapters.read_from_file(TRANSITION_EXAMPLE_PATH) - - otio.adapters.write_to_file(timeline, self.tmp_path) - self.assertTrue(os.path.exists(self.tmp_path)) - - with open(self.tmp_path) as fo: - test_data = fo.read() - - with open(BASELINE_TRANSITION_PATH) as fo: - baseline_data = fo.read() - - self.maxDiff = None - self._connectionFreeAssertMultiLineEqual(baseline_data, test_data) - - def test_image_sequence_example(self): - # SETUP - timeline = otio.adapters.read_from_file(IMAGE_SEQUENCE_EXAMPLE_PATH) - - # EXERCISE - otio.adapters.write_to_file(timeline, self.tmp_path) - - # VERIFY - self.assertTrue(os.path.exists(self.tmp_path)) - - with open(self.tmp_path) as f: - rv_session = f.read() - - self.assertEqual( - rv_session.count( - 'string movie = "./sample_sequence/sample_sequence.%04d.exr"' - ), - 1 - ) - - def test_transition_rvsession_covers_entire_shots(self): - # SETUP - timeline = otio.adapters.read_from_string(SAMPLE_DATA, "otio_json") - - # EXERCISE - otio.adapters.write_to_file(timeline, self.tmp_path) - - # VERIFY - with open(self.tmp_path, "r") as f: - rv_session = f.read() - - self.assertEqual(rv_session.count('movie = "blank'), 1) - self.assertEqual(rv_session.count('movie = "smpte'), 1) - - def test_audio_video_tracks(self): - # SETUP - timeline = otio.adapters.read_from_string(AUDIO_VIDEO_SAMPLE_DATA, "otio_json") - - # EXERCISE - otio.adapters.write_to_file(timeline, self.tmp_path) - - # VERIFY - self.assertTrue(os.path.exists(self.tmp_path)) - - audio_video_source = ( - 'string movie = ' - '[ "blank,start=0.0,end=499.0,fps=25.0.movieproc" ' - '"blank,start=0.0,end=499.0,fps=25.0.movieproc"' - ' "/path/to/audio.wav" ]' - ) - - with open(self.tmp_path, "r") as f: - rv_session = f.read() - - self.assertEqual(rv_session.count("string movie"), 2) - self.assertEqual(rv_session.count("blank"), 2) - self.assertEqual(rv_session.count(audio_video_source), 1) - - def test_nested_stack(self): - # SETUP - timeline = otio.adapters.read_from_string( - NESTED_STACK_SAMPLE_DATA, - "otio_json" - ) - - # EXERCISE - otio.adapters.write_to_file(timeline, self.tmp_path) - - # VERIFY - self.assertTrue(os.path.exists(self.tmp_path)) - - audio_video_source = ( - 'string movie = ' - '[ "blank,start=0.0,end=237.0,fps=24.0.movieproc"' - ' "blank,start=0.0,end=237.0,fps=24.0.movieproc"' - ' "/path/to/some/audio.wav" ]' - ) - video_source = ( - 'string movie = "/path/to/some/video.mov"' - ) - - with open(self.tmp_path, "r") as f: - rv_session = f.read() - self.assertEqual(rv_session.count(video_source), 2) - self.assertEqual(rv_session.count(audio_video_source), 2) - - def test_metadata_read(self): - timeline = otio.adapters.read_from_file(METADATA_EXAMPLE_PATH) - tmp_path = tempfile.mkstemp(suffix=".rv", text=True)[1] - - otio.adapters.write_to_file(timeline, tmp_path) - self.assertTrue(os.path.exists(tmp_path)) - - with open(tmp_path) as fo: - test_data = fo.read() - - with open(METADATA_BASELINE_PATH) as fo: - baseline_data = fo.read() - - self.maxDiff = None - self._connectionFreeAssertMultiLineEqual(baseline_data, test_data) - - def _connectionFreeAssertMultiLineEqual(self, first, second): - ''' - The "connections" list order is not stable between python versions - so as a quick hack, simply remove it from our diff - ''' - def _removeConnections(string): - return os.linesep.join([line for line in string.splitlines() - if 'connections' not in line]) - self.assertMultiLineEqual(_removeConnections(first), - _removeConnections(second)) - - -if __name__ == '__main__': - unittest.main() diff --git a/contrib/opentimelineio_contrib/adapters/tests/tests_xges_adapter.py b/contrib/opentimelineio_contrib/adapters/tests/tests_xges_adapter.py index 5b0f11903..127e9f857 100644 --- a/contrib/opentimelineio_contrib/adapters/tests/tests_xges_adapter.py +++ b/contrib/opentimelineio_contrib/adapters/tests/tests_xges_adapter.py @@ -1,26 +1,5 @@ -# +# SPDX-License-Identifier: Apache-2.0 # Copyright Contributors to the OpenTimelineIO project -# -# Licensed under the Apache License, Version 2.0 (the "Apache License") -# with the following modification; you may not use this file except in -# compliance with the Apache License and the following modification to it: -# Section 6. Trademarks. is deleted and replaced with: -# -# 6. Trademarks. This License does not grant permission to use the trade -# names, trademarks, service marks, or product names of the Licensor -# and its affiliates, except as required to comply with Section 4(c) of -# the License and to reproduce the content of the NOTICE file. -# -# You may obtain a copy of the Apache License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the Apache License with the above modification is -# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -# KIND, either express or implied. See the Apache License for the specific -# language governing permissions and limitations under the Apache License. -# import os import tempfile @@ -28,7 +7,6 @@ from fractions import Fraction from xml.etree import ElementTree -from builtins import int import opentimelineio as otio import opentimelineio.test_utils as otio_test_utils from opentimelineio.schema import ( @@ -116,7 +94,7 @@ def _make_ges_marker( return ges_marker -class XgesElement(object): +class XgesElement: """ Generates an xges string to be converted to an otio timeline. """ @@ -163,7 +141,7 @@ def add_video_track(self, framerate=None): res_caps = \ r"video/x-raw\,\ width\=\(int\)300\,\ height\=\(int\)250" if framerate: - res_caps += r"\,\ framerate\=\(fraction\){}".format(framerate) + res_caps += fr"\,\ framerate\=\(fraction\){framerate}" track = ElementTree.SubElement( self.timeline, "track", { "caps": "video/x-raw(ANY)", @@ -279,7 +257,7 @@ def get_otio_timeline(self): return otio.adapters.read_from_string(string, "xges") -class CustomOtioAssertions(object): +class CustomOtioAssertions: """Custom Assertions to perform on otio objects""" @staticmethod @@ -287,7 +265,7 @@ def _typed_name(otio_obj): name = otio_obj.name if not name: name = '""' - return "{} {}".format(otio_obj.schema_name(), name) + return f"{otio_obj.schema_name()} {name}" @classmethod def _otio_id(cls, otio_obj): @@ -366,7 +344,7 @@ def assertOtioHasAttrPath(self, otio_obj, attr_path): if first: first = False attr_str += " " - attr_str += "[{:d}]".format(attr_name) + attr_str += f"[{attr_name:d}]" else: if not hasattr(val, attr_name): raise AssertionError( @@ -478,7 +456,7 @@ def assertOtioNumChildren(self, otio_obj, compare): self._val_str(compare))) -class OtioTest(object): +class OtioTest: """Tests to be used by OtioTestNode and OtioTestTree.""" @staticmethod @@ -616,7 +594,7 @@ def markers(cls, *marker_details): inst, otio_item, marker_details) -class OtioTestNode(object): +class OtioTestNode: """ An OtioTestTree Node that corresponds to some expected otio class. This holds information about the children of the node, as well as @@ -638,7 +616,7 @@ def __init__(self, expect_type, children=[], tests=[]): self.tests = tests -class OtioTestTree(object): +class OtioTestTree: """ Test an otio object has the correct type structure, and perform additional tests along the way.""" @@ -685,14 +663,14 @@ def _sub_test_compare(self, otio_obj, node): test(self.unittest_inst, otio_obj) -class CustomXgesAssertions(object): +class CustomXgesAssertions: """Custom Assertions to perform on a ges xml object""" @staticmethod def _xges_id(xml_el): - xges_id = "Element <{}".format(xml_el.tag) + xges_id = f"Element <{xml_el.tag}" for key, val in xml_el.attrib.items(): - xges_id += " {}='{}'".format(key, val) + xges_id += f" {key}='{val}'" xges_id += " /> " return xges_id @@ -755,7 +733,7 @@ def assertXgesNumElementsAtPathWithAttr( for key, val in attrs.items(): if key in ("start", "duration", "inpoint"): val *= GST_SECOND - path += "[@{}='{!s}']".format(key, val) + path += f"[@{key}='{val!s}']" return self.assertXgesNumElementsAtPath(xml_el, path, compare) def assertXgesOneElementAtPathWithAttr( @@ -838,7 +816,7 @@ def assertXgesStructureFieldEqual( xml_el, struct_name, field_name, field_type) # TODO: remove once python2 has ended if field_type == "string": - if type(val) is not str and isinstance(val, type(u"")): + if type(val) is not str and isinstance(val, str): val = val.encode("utf8") if isinstance(val, otio.core.SerializableObject): equal = val.is_equivalent_to(compare) @@ -1507,7 +1485,7 @@ def test_uri_clip_asset_properties_and_metadatas(self): def _subproject_asset_props_and_metas_for_type(self, extract_type): xges_el = self._make_nested_project() asset = xges_el.ressources.find( - "./asset[@extractable-type-name='{}']".format(extract_type)) + f"./asset[@extractable-type-name='{extract_type}']") self.assertIsNotNone(asset) asset_id = asset.get("id") self.assertIsNotNone(asset_id) @@ -2560,7 +2538,7 @@ def test_GstStructure_invalid_parse(self): SCHEMA.GstStructure.new_from_str("0name, prop=(int)4;") with self.assertRaises(otio.exceptions.OTIOError): SCHEMA.GstStructure.new_from_str( - "{}, prop=(int)4;".format(UTF8_NAME)) + f"{UTF8_NAME}, prop=(int)4;") with self.assertRaises(otio.exceptions.OTIOError): SCHEMA.GstStructure("0name", {"prop": ("int", 4)}) # invalid fieldnames: @@ -2708,8 +2686,8 @@ def SKIP_test_roundtrip_disk2mem2disk(self): # But the xml text on disk is not identical because otio has a subset # of features to xges and we drop all the nle specific preferences. - with open(XGES_EXAMPLE_PATH, "r") as original_file: - with open(tmp_path, "r") as output_file: + with open(XGES_EXAMPLE_PATH) as original_file: + with open(tmp_path) as output_file: self.assertNotEqual(original_file.read(), output_file.read()) diff --git a/contrib/opentimelineio_contrib/adapters/xges.py b/contrib/opentimelineio_contrib/adapters/xges.py index 2684aab81..37117679e 100644 --- a/contrib/opentimelineio_contrib/adapters/xges.py +++ b/contrib/opentimelineio_contrib/adapters/xges.py @@ -1,44 +1,16 @@ -# +# SPDX-License-Identifier: Apache-2.0 # Copyright Contributors to the OpenTimelineIO project -# -# Licensed under the Apache License, Version 2.0 (the "Apache License") -# with the following modification; you may not use this file except in -# compliance with the Apache License and the following modification to it: -# Section 6. Trademarks. is deleted and replaced with: -# -# 6. Trademarks. This License does not grant permission to use the trade -# names, trademarks, service marks, or product names of the Licensor -# and its affiliates, except as required to comply with Section 4(c) of -# the License and to reproduce the content of the NOTICE file. -# -# You may obtain a copy of the Apache License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the Apache License with the above modification is -# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -# KIND, either express or implied. See the Apache License for the specific -# language governing permissions and limitations under the Apache License. -# """OpenTimelineIO GStreamer Editing Services XML Adapter.""" import re import os import warnings import numbers -try: - from urllib.parse import quote - from urllib.parse import unquote - from urllib.parse import urlparse - from urllib.parse import parse_qs -except ImportError: # python2 - from urllib import quote - from urllib import unquote - from urlparse import urlparse - from urlparse import parse_qs - -from builtins import int +from urllib.parse import quote +from urllib.parse import unquote +from urllib.parse import urlparse +from urllib.parse import parse_qs + from fractions import Fraction from xml.etree import ElementTree from xml.dom import minidom @@ -52,7 +24,7 @@ "crossfade": otio.schema.TransitionTypes.SMPTE_Dissolve } # Two way map -_TRANSITION_MAP.update(dict([(v, k) for k, v in _TRANSITION_MAP.items()])) +_TRANSITION_MAP.update({v: k for k, v in _TRANSITION_MAP.items()}) class XGESReadError(otio.exceptions.OTIOError): @@ -63,7 +35,7 @@ class UnhandledValueError(otio.exceptions.OTIOError): """Received value is not handled.""" def __init__(self, name, value): otio.exceptions.OTIOError.__init__( - self, "Unhandled value {!r} for {}.".format(value, name)) + self, f"Unhandled value {value!r} for {name}.") class InvalidValueError(otio.exceptions.OTIOError): @@ -130,7 +102,7 @@ def _force_gst_structure_name(struct, struct_name, owner=""): """ if struct.name != struct_name: if owner: - start = "{}'s".format(owner) + start = f"{owner}'s" else: start = "The" warnings.warn( @@ -143,7 +115,7 @@ def _force_gst_structure_name(struct, struct_name, owner=""): # TODO: remove unicode_to_str once python2 has ended: def unicode_to_str(value): """If python2, returns unicode as a utf8 str""" - if type(value) is not str and isinstance(value, type(u"")): + if type(value) is not str and isinstance(value, str): value = value.encode("utf8") return value @@ -1297,7 +1269,7 @@ def _serialize_stack_to_ressource(self, otio_stack, ressources): # NOTE: asset_id must be unique for both the # GESTimeline and GESUriClip extractable types break - asset_id = orig_asset_id + "_{:d}".format(i) + asset_id = orig_asset_id + f"_{i:d}" # create a timeline asset asset = self._insert_new_sub_element( ressources, "asset", attrib={ @@ -1476,7 +1448,7 @@ def _get_properties_with_unique_name( for i in itertools.count(start=1): if tmpname not in self.all_names: break - tmpname = name + "_{:d}".format(i) + tmpname = name + f"_{i:d}" self.all_names.add(tmpname) self._set_structure_value(properties, "name", "string", tmpname) return properties @@ -2288,7 +2260,7 @@ def __init__(self, name=None, fields=None): self.set(key, *entry) def __repr__(self): - return "GstStructure({!r}, {!r})".format(self.name, self.fields) + return f"GstStructure({self.name!r}, {self.fields!r})" UNKNOWN_PREFIX = "[UNKNOWN]" @@ -2326,12 +2298,12 @@ def _field_to_str(self, key): else: self._check_type(_type) value = self.serialize_value(_type, value) - return "{}=({}){}".format(key, _type, value) + return f"{key}=({_type}){value}" def _fields_to_str(self): write = [] for key in self.fields: - write.append(", {}".format(self._field_to_str(key))) + write.append(f", {self._field_to_str(key)}") return "".join(write) def _name_to_str(self): @@ -2342,7 +2314,7 @@ def _name_to_str(self): def __str__(self): """Emulates gst_structure_to_string""" - return "{}{};".format(self._name_to_str(), self._fields_to_str()) + return f"{self._name_to_str()}{self._fields_to_str()};" def get_type_name(self, key): """Return the field type""" @@ -2614,7 +2586,7 @@ def _parse_range_list_array(cls, read): values.append(value) if not read: raise DeserializeError( - read, "ended before {} could be found".format(end)) + read, f"ended before {end} could be found") read = read[1:] # skip past 'end' match = cls.END_REGEX.match(read) # skip whitespace read = read[match.end("end"):] @@ -2732,7 +2704,7 @@ def new_from_str(cls, read): @staticmethod def _val_read_err(typ, val): raise DeserializeError( - val, "does not translated to the {} type".format(typ)) + val, f"does not translated to the {typ} type") @classmethod def deserialize_value(cls, _type, value): @@ -2873,7 +2845,7 @@ def _wrap_string(cls, read): if byte in cls.GST_ASCII_CHARS: ser_string_list.append(chr(byte)) elif byte < 0x20 or byte >= 0x7f: - ser_string_list.append("\\{:03o}".format(byte)) + ser_string_list.append(f"\\{byte:03o}") added_wrap = True else: ser_string_list.append("\\" + chr(byte)) @@ -3464,7 +3436,7 @@ def __str__(self): # GST_FEATURE_MEMORY_SYSTEM_MEMORY feature, since this # considered equal to being an empty features. # We do not seem to require this behaviour - write.append("({!s})".format(features)) + write.append(f"({features!s})") write.append(struct._fields_to_str()) return "".join(write) diff --git a/contrib/opentimelineio_contrib/application_plugins/rv/example_otio_reader/PACKAGE b/contrib/opentimelineio_contrib/application_plugins/rv/example_otio_reader/PACKAGE deleted file mode 100644 index 09ef4d7ed..000000000 --- a/contrib/opentimelineio_contrib/application_plugins/rv/example_otio_reader/PACKAGE +++ /dev/null @@ -1,26 +0,0 @@ -package: Example OTIO Reader -author: Contributors to the OpenTimelineIO project -organization: OpenTimelineIO project -contact: otio-discussion@lists.aswf.io -version: 1.0 -url: http://opentimeline.io -rv: 4.0.8 - -modes: - - file: example_otio_reader_plugin.py - load: immediate - - file: otio_reader.py - location: Python - - -description: | -

- Example plugin that can read files supported by otio from within rv. This is - meant only as proof of concept. -

-

- To create the package run: -

-

- zip example_otio_reader_plugin-1.0.rvpkg PACKAGE example_otio_reader_plugin.py otio_reader.py -

diff --git a/contrib/opentimelineio_contrib/application_plugins/rv/example_otio_reader/example_otio_reader_plugin.py b/contrib/opentimelineio_contrib/application_plugins/rv/example_otio_reader/example_otio_reader_plugin.py deleted file mode 100644 index 011d26ee3..000000000 --- a/contrib/opentimelineio_contrib/application_plugins/rv/example_otio_reader/example_otio_reader_plugin.py +++ /dev/null @@ -1,168 +0,0 @@ -""" -Example plugin showing how otio files can be loaded into an RV context -""" -# -# Copyright Contributors to the OpenTimelineIO project -# -# Licensed under the Apache License, Version 2.0 (the "Apache License") -# with the following modification; you may not use this file except in -# compliance with the Apache License and the following modification to it: -# Section 6. Trademarks. is deleted and replaced with: -# -# 6. Trademarks. This License does not grant permission to use the trade -# names, trademarks, service marks, or product names of the Licensor -# and its affiliates, except as required to comply with Section 4(c) of -# the License and to reproduce the content of the NOTICE file. -# -# You may obtain a copy of the Apache License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the Apache License with the above modification is -# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -# KIND, either express or implied. See the Apache License for the specific -# language governing permissions and limitations under the Apache License. -# - -import os - -from rv import commands -from rv import rvtypes - -import opentimelineio as otio - -import otio_reader - - -class Mode(object): - sleeping = 1 - loading = 2 - processing = 3 - - -class ExampleOTIOReaderPlugin(rvtypes.MinorMode): - def __init__(self): - super(ExampleOTIOReaderPlugin, self).__init__() - self.init("example_otio_reader", - [("incoming-source-path", - self.incoming_source_path, - "Catch otio supported files and return movieproc"), - ("source-group-complete", - self.source_group_complete, - "Expand otio supported files (synchronous load)"), - ("after-progressive-loading", - self.after_progressive_loading, - "Expand otio supported files (asynchronous load)")], - None) - - # Start as sleeping - self.mode = Mode.sleeping - - def incoming_source_path(self, event): - """ - Detects if a file supported by otio is being loaded, and replaces - it with an empty movie proc containing an otioFile tag. This will be - replaced in expand_sources(). - """ - event.reject() - - parts = event.contents().split(';') - in_path = parts[0] - _, ext = os.path.splitext(in_path) - if ext: - ext = ext[1:] - - if ext in otio.adapters.suffixes_with_defined_adapters(read=True): - self.mode = Mode.loading - movieproc = 'blank,otioFile={}.movieproc'.format(in_path) - event.setReturnContent(movieproc) - - def after_progressive_loading(self, event): - """ - After progress loading event, is used for asynchronous addSource - file loading. - """ - event.reject() - - if self.mode != Mode.loading: - return - self.mode = Mode.processing - self.expand_sources() - - def source_group_complete(self, event): - """ - Source group complete event, is used for synchronous addSource - file loading. - """ - event.reject() - if self.mode == Mode.sleeping: - # this plugin isn't doing anything - return - - if self.mode == Mode.processing: - # already processing otio - return - - if commands.loadTotal() > 0: - # async load - return - - self.mode = Mode.processing - self.expand_sources() - - def expand_sources(self): - """ - Expand any movie movieproc otioFile sources. - """ - # disable caching for load speed - cache_mode = commands.cacheMode() - commands.setCacheMode(commands.CacheOff) - - try: - # find sources with a movieproc with an otioFile=foo.otio tag - default_inputs, _ = commands.nodeConnections('defaultSequence') - for src in commands.nodesOfType('RVSource'): - src_group = commands.nodeGroup(src) - if src_group not in default_inputs: - # not in default sequence, already processed - continue - - # get the source file name - paths = [info['file'] for info in - commands.sourceMediaInfoList(src) if 'file' in info] - for info_path in paths: - # Looking for: 'blank,otioFile=/foo.otio.movieproc' - parts = info_path.split("=", 1) - itype = parts[0] - if not itype.startswith('blank,otioFile'): - continue - # remove the .movieproc extension - path, _ = os.path.splitext(parts[1]) - - # remove temp movieproc source from current view, and all - # the default views - _remove_source_from_views(src_group) - - result = otio_reader.read_otio_file(path) - commands.setViewNode(result) - break - finally: - # turn cache mode back on and go back to sleep - commands.setCacheMode(cache_mode) - self.mode = Mode.sleeping - - -def _remove_source_from_views(source_group): - """ - Remove a source group from all views. - """ - for view in commands.viewNodes(): - view_inputs = commands.nodeConnections(view)[0] - if source_group in view_inputs: - view_inputs.remove(source_group) - commands.setNodeInputs(view, view_inputs) - - -def createMode(): - return ExampleOTIOReaderPlugin() diff --git a/contrib/opentimelineio_contrib/application_plugins/rv/example_otio_reader/otio_reader.py b/contrib/opentimelineio_contrib/application_plugins/rv/example_otio_reader/otio_reader.py deleted file mode 100644 index 0b74eb563..000000000 --- a/contrib/opentimelineio_contrib/application_plugins/rv/example_otio_reader/otio_reader.py +++ /dev/null @@ -1,425 +0,0 @@ -# -# Copyright Contributors to the OpenTimelineIO project -# -# Licensed under the Apache License, Version 2.0 (the "Apache License") -# with the following modification; you may not use this file except in -# compliance with the Apache License and the following modification to it: -# Section 6. Trademarks. is deleted and replaced with: -# -# 6. Trademarks. This License does not grant permission to use the trade -# names, trademarks, service marks, or product names of the Licensor -# and its affiliates, except as required to comply with Section 4(c) of -# the License and to reproduce the content of the NOTICE file. -# -# You may obtain a copy of the Apache License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the Apache License with the above modification is -# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -# KIND, either express or implied. See the Apache License for the specific -# language governing permissions and limitations under the Apache License. -# - -# This code has been taken from opentimelineio's exten_rv rv adapter -# and converted to work interactively in RV. -# -# TODO: We would like to move this back into opentimelineio's rv adapter -# such that we can use this both interactively as well as standalone -# - -from rv import commands -from rv import extra_commands - -import opentimelineio as otio -from contextlib import contextmanager - - -@contextmanager -def set_context(context, **kwargs): - old_context = context.copy() - context.update(**kwargs) - - try: - yield - finally: - context.clear() - context.update(old_context) - - -class NoMappingForOtioTypeError(otio.exceptions.OTIOError): - pass - - -def read_otio_file(otio_file): - """ - Main entry point to expand a given otio (or file otio can read) - into the current RV session. - - Returns the top level node created that represents this otio - timeline. - """ - input_otio = otio.adapters.read_from_file(otio_file) - - return create_rv_node_from_otio(input_otio) - - -def create_rv_node_from_otio(otio_obj, context=None): - WRITE_TYPE_MAP = { - otio.schema.Timeline: _create_timeline, - otio.schema.Stack: _create_stack, - otio.schema.Track: _create_track, - otio.schema.Clip: _create_item, - otio.schema.Gap: _create_item, - otio.schema.Transition: _create_transition, - otio.schema.SerializableCollection: _create_collection, - } - - if type(otio_obj) in WRITE_TYPE_MAP: - return WRITE_TYPE_MAP[type(otio_obj)](otio_obj, context) - - raise NoMappingForOtioTypeError( - str(type(otio_obj)) + " on object: {}".format(otio_obj) - ) - - -def _create_dissolve(pre_item, in_dissolve, post_item, context=None): - rv_trx = commands.newNode("CrossDissolve", in_dissolve.name or "dissolve") - extra_commands.setUIName(rv_trx, str(in_dissolve.name or "dissolve")) - - commands.setFloatProperty(rv_trx + ".parameters.startFrame", [1.0], True) - - num_frames = (in_dissolve.in_offset + in_dissolve.out_offset).rescaled_to( - pre_item.trimmed_range().duration.rate - ).value - - commands.setFloatProperty(rv_trx + ".parameters.numFrames", - [float(num_frames)], - True) - - commands.setFloatProperty(rv_trx + ".output.fps", - [float(pre_item.trimmed_range().duration.rate)], - True) - - pre_item_rv = create_rv_node_from_otio(pre_item, context) - - post_item_rv = create_rv_node_from_otio(post_item, context) - - node_to_insert = post_item_rv - - if ( - hasattr(pre_item, "media_reference") and - pre_item.media_reference and - pre_item.media_reference.available_range and - hasattr(post_item, "media_reference") and - post_item.media_reference and - post_item.media_reference.available_range and - ( - post_item.media_reference.available_range.start_time.rate != - pre_item.media_reference.available_range.start_time.rate - ) - ): - # write a retime to make sure post_item is in the timebase of pre_item - rt_node = commands.newNode("Retime", "transition_retime") - rt_node.setTargetFps( - pre_item.media_reference.available_range.start_time.rate - ) - - post_item_rv = create_rv_node_from_otio(post_item, context) - - rt_node.addInput(post_item_rv) - node_to_insert = rt_node - - commands.setNodeInputs(rv_trx, [pre_item_rv, node_to_insert]) - _add_metadata_to_node(in_dissolve, rv_trx) - return rv_trx - - -def _create_transition(pre_item, in_trx, post_item, context=None): - trx_map = { - otio.schema.TransitionTypes.SMPTE_Dissolve: _create_dissolve, - } - - if in_trx.transition_type not in trx_map: - return - - return trx_map[in_trx.transition_type]( - pre_item, - in_trx, - post_item, - context - ) - - -def _create_stack(in_stack, context=None): - new_stack = commands.newNode("RVStackGroup", in_stack.name or "tracks") - extra_commands.setUIName(new_stack, str(in_stack.name or "tracks")) - - new_inputs = [] - for seq in in_stack: - result = create_rv_node_from_otio(seq, context) - if result: - new_inputs.append(result) - - commands.setNodeInputs(new_stack, new_inputs) - _add_metadata_to_node(in_stack, new_stack) - return new_stack - - -def _create_track(in_seq, context=None): - context = context or {} - - new_seq = commands.newNode("RVSequenceGroup", str(in_seq.name or "track")) - extra_commands.setUIName(new_seq, str(in_seq.name or "track")) - - items_to_serialize = otio.algorithms.track_with_expanded_transitions( - in_seq - ) - - with set_context(context, track_kind=in_seq.kind): - new_inputs = [] - for thing in items_to_serialize: - if isinstance(thing, tuple): - result = _create_transition(*thing, context=context) - elif thing.duration().value == 0: - continue - else: - result = create_rv_node_from_otio(thing, context) - - if result: - new_inputs.append(result) - - commands.setNodeInputs(new_seq, new_inputs) - _add_metadata_to_node(in_seq, new_seq) - - return new_seq - - -def _get_global_transform(tl): - # since there's no global scale in otio, use the first source with - # bounds as the global bounds - def find_display_bounds(tl): - for clip in tl.clip_if(): - try: - bounds = clip.media_reference.available_image_bounds - if bounds: - return bounds - except AttributeError: - continue - return None - - bounds = find_display_bounds(tl) - if bounds is None: - return {} - - translate = bounds.center() - scale = bounds.max - bounds.min - - # RV's global coordinate system has a width and height of 1 where the - # width will be scaled to the image aspect ratio. So scale globally by - # height. The source width will later be scaled to aspect ratio. - global_scale = otio.schema.V2d(1.0 / scale.y, 1.0 / scale.y) - - return { - 'global_scale': global_scale, - 'global_translate': translate * global_scale, - } - - -def _create_timeline(tl, context=None): - context = context or {} - - with set_context( - context, - **_get_global_transform(tl) - ): - return create_rv_node_from_otio(tl.tracks, context) - - -def _create_collection(collection, context=None): - results = [] - for item in collection: - result = create_rv_node_from_otio(item, context) - if result: - results.append(result) - - if results: - return results[0] - - -def _create_media_reference(item, context=None): - context = context or {} - if hasattr(item, "media_reference") and item.media_reference: - if isinstance(item.media_reference, otio.schema.ExternalReference): - media = [str(item.media_reference.target_url)] - if context.get('track_kind') == otio.schema.TrackKind.Audio: - # Create blank video media to accompany audio for valid source - blank = _create_movieproc(item.available_range()) - # Appending blank to media promotes name of audio file in RV - media.append(blank) - - return media - elif isinstance(item.media_reference, - otio.schema.ImageSequenceReference): - frame_sub = "%0{n}d".format( - n=item.media_reference.frame_zero_padding - ) - - media = [ - str(item.media_reference.abstract_target_url(symbol=frame_sub)) - ] - - return media - elif isinstance(item.media_reference, otio.schema.GeneratorReference): - if item.media_reference.generator_kind == "SMPTEBars": - kind = "smptebars" - return [_create_movieproc(item.available_range(), kind)] - - return None - - -def _create_item(it, context=None): - context = context or {} - range_to_read = it.trimmed_range() - - if not range_to_read: - raise otio.exceptions.OTIOError( - "No valid range on clip: {0}.".format( - str(it) - ) - ) - - new_media = _create_media_reference(it, context) - if not new_media: - kind = "smptebars" - if isinstance(it, otio.schema.Gap): - kind = "blank" - new_media = [_create_movieproc(range_to_read, kind)] - - try: - src = commands.addSourceVerbose(new_media) - except Exception as e: - # Perhaps the media was missing, if so, lets load an error - # source - print('ERROR: {}'.format(e)) - error_media = _create_movieproc(range_to_read, 'smptebars') - src = commands.addSourceVerbose([error_media]) - - src_group = commands.nodeGroup(src) - - extra_commands.setUIName(src_group, str(it.name or "clip")) - - # Add otio metadata to this group and the source - _add_metadata_to_node(it, src_group) - if hasattr(it, "media_reference") and it.media_reference: - _add_metadata_to_node(it.media_reference, src) - - in_frame = out_frame = None - if hasattr(it, "media_reference") and it.media_reference: - if isinstance(it.media_reference, otio.schema.ImageSequenceReference): - in_frame, out_frame = \ - it.media_reference.frame_range_for_time_range( - range_to_read - ) - - _add_source_bounds(it.media_reference, src, context) - - if not in_frame and not out_frame: - # because OTIO has no global concept of FPS, the rate of the duration - # is used as the rate for the range of the source. - in_frame = otio.opentime.to_frames( - range_to_read.start_time, - rate=range_to_read.duration.rate - ) - out_frame = otio.opentime.to_frames( - range_to_read.end_time_inclusive(), - rate=range_to_read.duration.rate - ) - - commands.setIntProperty(src + ".cut.in", [in_frame]) - commands.setIntProperty(src + ".cut.out", [out_frame]) - - commands.setFloatProperty(src + ".group.fps", - [float(range_to_read.duration.rate)]) - - return src_group - - -def _create_movieproc(time_range, kind="blank"): - movieproc = "{},start={},end={},fps={}.movieproc".format( - kind, - time_range.start_time.value, - time_range.end_time_inclusive().value, - time_range.duration.rate - ) - return movieproc - - -def _add_source_bounds(media_ref, src, context): - bounds = media_ref.available_image_bounds - if not bounds: - return - - global_scale = context.get('global_scale') - global_translate = context.get('global_translate') - if global_scale is None or global_translate is None: - return - - # A width of 1.0 in RV means draw to the aspect ratio, so scale the - # width by the inverse of the aspect ratio - # - media_info = commands.sourceMediaInfo(src) - height = media_info['height'] - aspect_ratio = 1.0 if height == 0 else media_info['width'] / height - - translate = bounds.center() * global_scale - global_translate - scale = (bounds.max - bounds.min) * global_scale - - transform_node = extra_commands.associatedNode('RVTransform2D', src) - - commands.setFloatProperty( - "{}.transform.scale".format(transform_node), - [scale.x / aspect_ratio, scale.y] - ) - commands.setFloatProperty( - "{}.transform.translate".format(transform_node), - [translate.x, translate.y] - ) - - # write the bounds global_scale and global_translate to the node so we can - # preserve the original values if we round-trip - commands.newProperty( - "{}.otio.global_scale".format(transform_node), - commands.FloatType, - 2 - ) - commands.newProperty( - "{}.otio.global_translate".format(transform_node), - commands.FloatType, - 2 - ) - commands.setFloatProperty( - "{}.otio.global_scale".format(transform_node), - [global_scale.x, global_scale.y], - True - ) - commands.setFloatProperty( - "{}.otio.global_translate".format(transform_node), - [global_translate.x, global_translate.y], - True - ) - - -def _add_metadata_to_node(item, rv_node): - """ - Add metadata from otio "item" to rv_node - """ - if item.metadata: - otio_metadata_property = rv_node + ".otio.metadata" - otio_metadata = otio.core.serialize_json_to_string(item.metadata, - indent=-1) - commands.newProperty(otio_metadata_property, commands.StringType, 1) - commands.setStringProperty(otio_metadata_property, - [otio_metadata], - True) diff --git a/contrib/opentimelineio_contrib/application_plugins/tests/test_rv_reader.py b/contrib/opentimelineio_contrib/application_plugins/tests/test_rv_reader.py deleted file mode 100644 index 75570212c..000000000 --- a/contrib/opentimelineio_contrib/application_plugins/tests/test_rv_reader.py +++ /dev/null @@ -1,344 +0,0 @@ -# -# Copyright Contributors to the OpenTimelineIO project -# -# Licensed under the Apache License, Version 2.0 (the "Apache License") -# with the following modification; you may not use this file except in -# compliance with the Apache License and the following modification to it: -# Section 6. Trademarks. is deleted and replaced with: -# -# 6. Trademarks. This License does not grant permission to use the trade -# names, trademarks, service marks, or product names of the Licensor -# and its affiliates, except as required to comply with Section 4(c) of -# the License and to reproduce the content of the NOTICE file. -# -# You may obtain a copy of the Apache License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the Apache License with the above modification is -# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -# KIND, either express or implied. See the Apache License for the specific -# language governing permissions and limitations under the Apache License. -# - -"""Unit tests for the rv reader plugin""" - -# Please note that the test is geared towards linux install as I don't have -# a Windows or Mac available to me. - -import os -import sys -import ast -import socket -import zipfile -import tempfile -import unittest -import shutil -import shlex -import time -import imp -import platform -from subprocess import call, Popen, PIPE - -import opentimelineio as otio - -RV_OTIO_READER_NAME = 'Example OTIO Reader' -RV_OTIO_READER_VERSION = '1.0' - -RV_ROOT_DIR = os.getenv('OTIO_RV_ROOT_DIR', '') -RV_BIN_DIR = os.path.join( - RV_ROOT_DIR, - 'MacOS' if platform.system() == 'Darwin' else 'bin' -) - -RV_OTIO_READER_DIR = os.path.join( - '..', - 'rv', - 'example_otio_reader' -) - -# Import rvNetwork with imp to compensate for older RV's missing __init__.py -RV_NETWORK_MODULE = os.path.join( - RV_ROOT_DIR, - 'src', - 'python', - 'network', - 'rvNetwork.py' -) -rvNetwork = imp.load_source('rvNetwork', RV_NETWORK_MODULE) - -# Generate sample data -sample_timeline = otio.schema.Timeline( - 'my_timeline', - global_start_time=otio.opentime.RationalTime(1, 24) -) -track = otio.schema.Track('v1') -bounds = [ - otio.schema.Box2d( - otio.schema.V2d(0.0, 0.0), - otio.schema.V2d(16.0, 9.0) - ), # sets viewing area - otio.schema.Box2d( - otio.schema.V2d(8.0, 0), - otio.schema.V2d(24.0, 9.0) - ), # shifted right by half the viewing area - otio.schema.Box2d( - otio.schema.V2d(0.0, 0.0), - otio.schema.V2d(8.0, 4.5) - ) # scale to 1/4 of viewing area (lower left) -] - -for clipnum, box in zip(range(1, 4), bounds): - clip_name = 'clip{n}'.format(n=clipnum) - track.append( - otio.schema.Clip( - clip_name, - media_reference=otio.schema.ExternalReference( - target_url="{clip_name}.mov".format(clip_name=clip_name), - available_range=otio.opentime.TimeRange( - otio.opentime.RationalTime(1, 24), - otio.opentime.RationalTime(50, 24) - ), - available_image_bounds=box - ), - source_range=otio.opentime.TimeRange( - otio.opentime.RationalTime(11, 24), - otio.opentime.RationalTime(3, 24) - ) - ) - ) -sample_timeline.tracks.append(track) - - -@unittest.skipIf( - "OTIO_RV_ROOT_DIR" not in os.environ, - "OTIO_RV_ROOT_DIR not set." -) -@unittest.skipIf( - (sys.version_info > (3, 0)), - "RV OTIO reader plugin does not work in python 3." -) -class RVSessionAdapterReadTest(unittest.TestCase): - def create_temp_dir(self): - return tempfile.mkdtemp(prefix='rv_otio_reader') - - def test_create_rvpkg(self): - temp_dir = self.create_temp_dir() - package_path = create_rvpkg(temp_dir) - - self.assertTrue(os.path.exists(package_path)) - self.assertTrue(os.path.getsize(package_path) > 0) - - # Cleanup - shutil.rmtree(temp_dir) - - def test_install_plugin(self): - temp_dir = self.create_temp_dir() - source_package_path = create_rvpkg(temp_dir) - - # Install package - rc = install_package(source_package_path) - - # Check if install succeeded - installed_package_path = os.path.join( - temp_dir, - 'Packages', - os.path.basename(source_package_path) - ) - - self.assertTrue(rc == 0) - self.assertTrue(len(os.listdir(temp_dir)) > 1) - self.assertTrue(os.path.exists(installed_package_path)) - - # Make sure package is available in RV - list_cmd = '{root}/rvpkg ' \ - '-only {tmp_dir} -list'\ - .format( - root=RV_BIN_DIR, - tmp_dir=temp_dir - ) - - proc = Popen(shlex.split(list_cmd), stdout=PIPE) - stdout, _ = proc.communicate() - - desired_result = \ - 'I L - {version} "{package_name}" {pkg_path}'.format( - version=RV_OTIO_READER_VERSION, - package_name=RV_OTIO_READER_NAME, - pkg_path=os.path.realpath(installed_package_path) - ) - - self.assertIn(desired_result, stdout.split('\n')) - - # Cleanup - shutil.rmtree(temp_dir) - - def test_read_otio_file(self): - # Install package - temp_dir = self.create_temp_dir() - source_package_path = create_rvpkg(temp_dir) - install_package(source_package_path) - - env = os.environ.copy() - env.update({'RV_SUPPORT_PATH': temp_dir}) - - sample_file = tempfile.NamedTemporaryFile( - 'w', - prefix='otio_data_', - suffix='.otio', - dir=temp_dir, - delete=False - ) - otio.adapters.write_to_file(sample_timeline, sample_file.name) - run_cmd = '{root}/{exe} ' \ - '-nc ' \ - '-network ' \ - '-networkHost localhost ' \ - '-networkPort {port} ' \ - '{sample_file}' \ - .format( - exe='RV' if platform.system() == 'Darwin' else 'rv', - root=RV_BIN_DIR, - port=9876, - sample_file=sample_file.name - ) - proc = Popen(shlex.split(run_cmd), env=env) - - # Connect with RV - rvc = rvNetwork.RvCommunicator() - - try: - attempts = 0 - while not rvc.connected: - attempts += 1 - rvc.connect('localhost', 9876) - - if not rvc.connected: - time.sleep(.5) - - if attempts == 20: - raise socket.error( - "Unable to connect to RV!" - ) - - # some time can pass between the RV connection - # and the complete startup of RV - print("Waiting for RV startup to complete") - time.sleep(10) - - # Check clips at positions - clip1 = rv_media_name_at_frame(rvc, 1) - self.assertEqual(clip1, 'clip1.mov') - - # note RV has a default res of 1280,720 when the media doesn't exist - aspect_ratio = 1280.0 / 720.0 - - clip1_scale, clip1_translate = rv_transform_at_frame(rvc, 1) - self.assertEqual(clip1_scale, [1.0, 1.0]) - self.assertEqual(clip1_translate, [0.0, 0.0]) - - clip2 = rv_media_name_at_frame(rvc, 4) - self.assertEqual(clip2, 'clip2.mov') - - clip2_scale, clip2_translate = rv_transform_at_frame(rvc, 4) - self.assertEqual(clip2_scale, [1.0, 1.0]) - - self.assertAlmostEqual(clip2_translate[0], 0.5 * aspect_ratio) - self.assertEqual(clip2_translate[1], 0) - - clip3 = rv_media_name_at_frame(rvc, 7) - self.assertEqual(clip3, 'clip3.mov') - - clip3_scale, clip3_translate = rv_transform_at_frame(rvc, 7) - self.assertEqual(clip3_scale, [0.5, 0.5]) - - self.assertAlmostEqual(clip3_translate[0], -0.25 * aspect_ratio) - self.assertEqual(clip3_translate[1], -0.25) - - rvc.disconnect() - - finally: - # Cleanup - proc.terminate() - shutil.rmtree(temp_dir) - - -def create_rvpkg(temp_dir): - package_path = os.path.join( - temp_dir, - 'example_otio_reader_plugin-{version}.rvpkg' - .format(version=RV_OTIO_READER_VERSION) - ) - with zipfile.ZipFile(package_path, 'w') as pkg: - for item in os.listdir(RV_OTIO_READER_DIR): - pkg.write(os.path.join(RV_OTIO_READER_DIR, item), item) - - return package_path - - -def install_package(source_package_path): - install_cmd = '{root}/rvpkg -force ' \ - '-install ' \ - '-add {tmp_dir} ' \ - '{pkg_file}'.format( - root=RV_BIN_DIR, - tmp_dir=os.path.dirname(source_package_path), - pkg_file=source_package_path - ) - rc = call(shlex.split(install_cmd)) - return rc - - -def _exec_command(rvc, command, literal=True): - response = rvc.sendEventAndReturn("remote-pyeval", command) - return ast.literal_eval(response) if literal else response - - -def _source_at_frame(rvc, frame): - return _exec_command( - rvc, - "rv.commands.sourcesAtFrame({0})".format(frame) - )[0] - - -def rv_media_name_at_frame(rvc, frame): - source_name = _source_at_frame(rvc, frame) - return _exec_command( - rvc, - "rv.commands.sourceMedia('{0}')".format(source_name) - )[0] - - -def rv_transform_at_frame(rvc, frame): - source = _source_at_frame(rvc, frame) - - source_group = _exec_command( - rvc, - """rv.commands.nodeGroup('{0}')""".format(source), - literal=False - ) - - transform = _exec_command( - rvc, - """rv.extra_commands.nodesInGroupOfType( - '{0}', 'RVTransform2D')""".format(source_group) - )[0] - - scale = _exec_command( - rvc, - """rv.commands.getFloatProperty( - '{0}.transform.scale')""".format(transform) - ) - - translate = _exec_command( - rvc, - """rv.commands.getFloatProperty( - '{0}.transform.translate')""".format(transform) - ) - - return scale, translate - - -if __name__ == '__main__': - unittest.main() diff --git a/docs/Makefile b/docs/Makefile index 9e00093ce..65231371b 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -2,15 +2,15 @@ # # You can set these variables from the command line. -SPHINXOPTS = +SPHINXOPTS = -n -j8 SPHINXBUILD = sphinx-build PAPER = -BUILDDIR = build +BUILDDIR = _build # Internal variables. PAPEROPT_a4 = -D latex_paper_size=a4 PAPEROPT_letter = -D latex_paper_size=letter -ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source +ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . # the i18n builder cannot share the environment and doctrees with the others I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source @@ -18,8 +18,10 @@ I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source help: @echo "Please use \`make ' where is one of" @echo " html to make standalone HTML files" + @echo " htmlstrict to make standalone HTML files and fails if any warnings or errors are produced" @echo " dirhtml to make HTML files named index.html in directories" @echo " singlehtml to make a single large HTML file" + @echo " livehtml to make standalone HTML files served by a web server that will automatically reload and rebuild when a file changes" @echo " pickle to make pickle files" @echo " json to make JSON files" @echo " htmlhelp to make HTML files and a HTML help project" @@ -54,6 +56,12 @@ html: @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." +.PHONY: htmlstrict +htmlstrict: + $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/htmlstrict -W --keep-going -w $(BUILDDIR)/htmlstrict/output.txt + @echo + @echo "Warnings check complete." + .PHONY: dirhtml dirhtml: $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml @@ -66,6 +74,10 @@ singlehtml: @echo @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." +.PHONY: livehtml +livehtml: + sphinx-autobuild "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) + .PHONY: pickle pickle: $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle diff --git a/docs/_static/OpenTimelineIO_Logo.pdf b/docs/_static/OpenTimelineIO_Logo.pdf new file mode 100644 index 000000000..c227a630c Binary files /dev/null and b/docs/_static/OpenTimelineIO_Logo.pdf differ diff --git a/docs/_static/OpenTimelineIO_Logo.svg b/docs/_static/OpenTimelineIO_Logo.svg new file mode 100644 index 000000000..d7311f406 --- /dev/null +++ b/docs/_static/OpenTimelineIO_Logo.svg @@ -0,0 +1,73 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/_templates/autosummary/module.rst b/docs/_templates/autosummary/module.rst new file mode 100644 index 000000000..7792d1f71 --- /dev/null +++ b/docs/_templates/autosummary/module.rst @@ -0,0 +1,18 @@ +{{ fullname }} +{{ underline }} + +.. automodule:: {{ fullname }} + :members: + +{% block modules %} +{% if modules %} +.. rubric:: Modules + +.. autosummary:: + :toctree: + :recursive: +{% for item in modules %} + {{ item }} +{%- endfor %} +{% endif %} +{% endblock %} diff --git a/docs/conf.py b/docs/conf.py index 5c3181e8b..f83edac93 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -1,304 +1,189 @@ -# -*- coding: utf-8 -*- +# +# SPDX-License-Identifier: Apache-2.0 +# Copyright Contributors to the OpenTimelineIO project +import re import sphinx_rtd_theme import opentimelineio -PACKAGE_TITLE = 'OpenTimelineIO' -PACKAGE_NAME = 'opentimelineio' -PACKAGE_DIR = 'src/py-opentimelineio/opentimelineio' -AUTHOR_NAME = 'Contributors to the OpenTimelineIO project' +# -- Project information --------------------------------------------------------------- +# https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information + +project = 'OpenTimelineIO' +copyright = "Copyright Contributors to the OpenTimelineIO project" +author = 'Contributors to the OpenTimelineIO project' try: RELEASE = opentimelineio.__version__ except AttributeError: RELEASE = 'unknown' -# If extensions (or modules to document with autodoc) are in another directory, -# add these directories to sys.path here. If the directory is relative to the -# documentation root, use os.path.abspath to make it absolute, like shown here. -# sys.path.insert(0, os.path.abspath('.')) - -# -- General configuration ------------------------------------------------ - -# If your documentation needs a minimal Sphinx version, state it here. -# needs_sphinx = '1.0' - -# Add any Sphinx extension module names here, as strings. They can be -# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom -# ones. -extensions = [ - 'sphinx.ext.autodoc', - 'sphinx.ext.doctest', - # this plugin is used to format our markdown correctly - 'recommonmark', - # uncomment the next line if you are writing in Google Napoleon docstrings - # 'sphinx.ext.napoleon' -] - -autodoc_mock_imports = ['aaf'] - -# Add any paths that contain templates here, relative to this directory. -templates_path = ['_templates'] - -source_parsers = { - '.md': 'recommonmark.parser.CommonMarkParser', -} - -# The suffix of source filenames. -source_suffix = ['.rst', '.md'] - -# The encoding of source files. -# source_encoding = 'utf-8-sig' - -# The main toctree document. -master_doc = 'index' - -# General information about the project. -project = PACKAGE_TITLE -copyright = u"Copyright Contributors to the OpenTimelineIO project" - -# The version info for the project you're documenting, acts as replacement for -# |version| and |release|, also used in various other places throughout the -# built documents. -# # The short X.Y version. version = RELEASE.split('-')[0] # The full version, including alpha/beta/rc tags. release = RELEASE -# The language for content autogenerated by Sphinx. Refer to documentation -# for a list of supported languages. -# language = None - -# There are two options for replacing |today|: either, you set today to some -# non-false value, then it is used: -# today = '' - -# Else, today_fmt is used as the format for a strftime call. -# today_fmt = '%B %d, %Y' - -# List of patterns, relative to source directory, that match files and -# directories to ignore when looking for source files. -exclude_patterns = ['_build'] - -# The reST default role (used for this markup: `text`) to use for all -# documents. -# default_role = None +# -- General configuration ------------------------------------------------------------- +# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration -# If true, '()' will be appended to :func: etc. cross-reference text. -# add_function_parentheses = True +extensions = [ + 'sphinx.ext.autodoc', + 'sphinx.ext.autosummary', + 'sphinx.ext.intersphinx', + 'myst_parser', # This plugin is used to format our markdown correctly +] -# If true, the current module name will be prepended to all description -# unit titles (such as .. function::). -# add_module_names = True +templates_path = ['_templates'] -# If true, sectionauthor and moduleauthor directives will be shown in the -# output. They are ignored by default. -# show_authors = False +exclude_patterns = ['_build', '_templates', '.venv'] -# The name of the Pygments (syntax highlighting) style to use. pygments_style = 'sphinx' -# A list of ignored prefixes for module index sorting. -# modindex_common_prefix = [] -# If true, keep warnings as "system message" paragraphs in the built documents. -# keep_warnings = False - - -# -- Options for HTML output ---------------------------------------------- - -# The theme to use for HTML and HTML Help pages. See the documentation for -# a list of builtin themes. -# html_theme = 'default' - -# Theme options are theme-specific and customize the look and feel of a theme -# further. For a list of options available for each theme, see the -# documentation. -# html_theme_options = {} - -# Add any paths that contain custom themes here, relative to this directory. -# html_theme_path = [] +# -- Options for HTML output ----------------------------------------------------------- +# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output html_theme = "sphinx_rtd_theme" html_theme_path = [sphinx_rtd_theme.get_html_theme_path()] -# The name for this set of Sphinx documents. If None, it defaults to -# " v documentation". -# html_title = None - -# A shorter title for the navigation bar. Default is the same as html_title. -# html_short_title = None - -# The name of an image file (relative to this directory) to place at the top -# of the sidebar. -# html_logo = None - -# The name of an image file (within the static path) to use as favicon of the -# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 -# pixels large. -# html_favicon = None -# Add any paths that contain custom static files (such as style sheets) here, -# relative to this directory. They are copied after the builtin static files, -# so a file named "default.css" will overwrite the builtin "default.css". -html_static_path = ['_static'] +htmlhelp_basename = f'{project.lower()}doc' -# Add any extra paths that contain custom files (such as robots.txt or -# .htaccess) here, relative to this directory. These files are copied -# directly to the root of the documentation. -html_extra_path = [] -# If not '', a 'Last updated on:' timestamp is inserted at every page bottom, -# using the given strftime format. -# html_last_updated_fmt = '%b %d, %Y' +# -- Options for LaTeX output ---------------------------------------------------------- +# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-latex-output -# If true, SmartyPants will be used to convert quotes and dashes to -# typographically correct entities. -# html_use_smartypants = True - -# Custom sidebar templates, maps document names to template names. -# html_sidebars = {} - -# Additional templates that should be rendered to pages, maps page names to -# template names. -# html_additional_pages = {} - -# If false, no module index is generated. -# html_domain_indices = True - -# If false, no index is generated. -# html_use_index = True - -# If true, the index is split into individual pages for each letter. -# html_split_index = False - -# If true, links to the reST sources are added to the pages. -# html_show_sourcelink = True - -# If true, "Created using Sphinx" is shown in the HTML footer. Default is True. -# html_show_sphinx = True - -# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. -# html_show_copyright = True - -# If true, an OpenSearch description file will be output, and all pages will -# contain a tag referring to it. The value of this option must be the -# base URL from which the finished HTML is served. -# html_use_opensearch = '' - -# This is the file name suffix for HTML files (e.g. ".xhtml"). -# html_file_suffix = None - -# Output file base name for HTML help builder. -htmlhelp_basename = '{}doc'.format(PACKAGE_NAME) - - -# -- Options for LaTeX output --------------------------------------------- - -latex_elements = { - # The paper size ('letterpaper' or 'a4paper'). - # 'papersize': 'letterpaper', - - # The font size ('10pt', '11pt' or '12pt'). - # 'pointsize': '10pt', - - # Additional stuff for the LaTeX preamble. - # 'preamble': '', -} - -# Grouping the document tree into LaTeX files. List of tuples -# (source start file, target name, title, -# author, documentclass [howto, manual, or own class]). latex_documents = [ - ('index', '{}.tex'.format(PACKAGE_NAME), - u'{} Documentation'.format(PACKAGE_TITLE), - AUTHOR_NAME, 'manual'), + ('index', f'{project.lower()}.tex', + f'{project} Documentation', + author, 'manual'), ] -# The name of an image file (relative to this directory) to place at the top of -# the title page. -# latex_logo = None - -# For "manual" documents, if this is true, then toplevel headings are parts, -# not chapters. -# latex_use_parts = False - -# If true, show page references after internal links. -# latex_show_pagerefs = False - -# If true, show URL addresses after external links. -# latex_show_urls = False - -# Documents to append as an appendix to all manuals. -# latex_appendices = [] +# -- Options for manual page output ---------------------------------------------------- +# sphinx-doc.org/en/master/usage/configuration.html#options-for-manual-page-output -# If false, no module index is generated. -# latex_domain_indices = True - - -# -- Options for manual page output --------------------------------------- - -# One entry per manual page. List of tuples -# (source start file, name, description, authors, manual section). man_pages = [ - ('index', PACKAGE_NAME, u'{} Documentation'.format({PACKAGE_TITLE}), - [AUTHOR_NAME], 1) + ('index', project.lower(), f'{project} Documentation', + [author], 1) ] -# If true, show URL addresses after external links. -# man_show_urls = False - +# -- Options for Texinfo output -------------------------------------------------------- +# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-texinfo-output -# -- Options for Texinfo output ------------------------------------------- - -# Grouping the document tree into Texinfo files. List of tuples -# (source start file, target name, title, author, -# dir menu entry, description, category) texinfo_documents = [ - ('index', PACKAGE_NAME, u'{} Documentation'.format(PACKAGE_TITLE), - AUTHOR_NAME, PACKAGE_TITLE, 'One line description of project.', + ('index', project.lower(), f'{project} Documentation', + author, project, 'One line description of project.', 'Miscellaneous'), ] -# Documents to append as an appendix to all manuals. -# texinfo_appendices = [] +# -- Options for intersphinx ----------------------------------------------------------- +# https://www.sphinx-doc.org/en/master/usage/extensions/intersphinx.html#configuration + +intersphinx_mapping = { + "python": ("https://docs.python.org/3", None), +} -# If false, no module index is generated. -# texinfo_domain_indices = True +# -- Options for Autodoc --------------------------------------------------------------- +# https://www.sphinx-doc.org/en/master/usage/extensions/autodoc.html#configuration -# How to display URL addresses: 'footnote', 'no', or 'inline'. -# texinfo_show_urls = 'footnote' +# Both the class’ and the __init__ method’s docstring are concatenated and inserted. +# Pybind11 generates class signatures on the __init__ method. +autoclass_content = "both" -# If true, do not generate a @detailmenu in the "Top" node's menu. -# texinfo_no_detailmenu = False +autodoc_default_options = { + 'undoc-members': True +} +# -- Options for linkcheck ------------------------------------------------------------- +# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-the-linkcheck-builder -def run_apidoc(_): - """This method is required by the setup method below.""" - ignore_paths = ['opentimelineio_contrib.adapters', 'tests', 'setup.py'] - # https://github.com/sphinx-doc/sphinx/blob/master/sphinx/ext/apidoc.py - argv = [ - '--force', - '--no-toc', - # '--separate', - '--module-first', - '--output-dir', - './api/modules', - # this is expected to be run from the `docs` directory. Its possible - # to set it up to run from other places, but so far this has worked for - # us - '../src/py-opentimelineio/opentimelineio', - ] + ignore_paths +linkcheck_exclude_documents = [ + r'cxx/cxx' +] - from sphinx.ext import apidoc - apidoc.main(argv) +# -- Options for MySt-Parser ----------------------------------------------------------- +# https://myst-parser.readthedocs.io/en/latest/sphinx/reference.html + +myst_heading_anchors = 5 + +# -- Custom ---------------------------------------------------------------------------- + +def process_signature( + app, + what: str, + name: str, + obj: object, + options: dict[str, str], + signature: str, + return_annotation, +): + """This does several things: + * Removes "self" argument from a signature. Pybind11 adds self to + method arguments, which is useless in a python reference documentation. + * Handles overloaded methods/functions by using the docstrings generated + by Pybind11. Pybind11 adds the signature of each overload in the first function's + signature. So the idea is to generate a new signature for each one instead. + """ + signatures = [] + isClass = what == "class" + + # This block won't be necessary once https://github.com/pybind/pybind11/pull/2621 + # gets merged in Pybind11. + if signature or isClass: + docstrLines = obj.__doc__ and obj.__doc__.split("\n") or [] + if not docstrLines or isClass: + # A class can have part of its doc in its docstr or in the __init__ docstr. + docstrLines += ( + obj.__init__.__doc__ and obj.__init__.__doc__.split("\n") or [] + ) + + # This could be solidified by using a regex on the reconstructed docstr? + if len(docstrLines) > 1 and "Overloaded function." in docstrLines: + # Overloaded function detected. Extract each signature and create a new + # signature for each of them. + for line in docstrLines: + nameToMatch = name.split(".")[-1] if not isClass else "__init__" + + # Maybe get use sphinx.util.inspect.signature_from_str ? + if match := re.search(fr"^\d+\.\s{nameToMatch}(\(.*)", line): + signatures.append(match.group(1)) + elif signature: + signatures.append(signature) + + signature = "" + + # Remove self from signatures. + for index, sig in enumerate(signatures): + newsig = re.sub(r"self\: [a-zA-Z0-9._]+(,\s)?", "", sig) + signatures[index] = newsig + + signature = "\n".join(signatures) + return signature, return_annotation + + +def process_docstring( + app, + what: str, + name: str, + obj: object, + options: dict[str, str], + lines: list[str], +): + for index, line in enumerate(lines): + # Remove "self" from docstrings of overloaded functions/methods. + # For overloaded functions/methods/classes, pybind11 + # creates docstrings that look like: + # + # Overloaded function. + # 1. func_name(self: , param2: int) + # 1. func_name(self: , param2: float) + # + # "self" is a distraction that can be removed to improve readability. + # This should be removed once https://github.com/pybind/pybind11/pull/2621 is merged. + if re.match(fr'\d+\. {name.split("."[0])}', line): + line = re.sub(r"self\: [a-zA-Z0-9._]+(,\s)?", "", line) + lines[index] = line def setup(app): - """This method is a hook into the Sphinx builder system and injects the - apidoc module into it so it runs autodoc before running build. - - If you mess with this, you may not see any effect in a local build, this - was added to get api documentation building on the ReadTheDocs server. - """ - app.connect('builder-inited', run_apidoc) + app.connect("autodoc-process-signature", process_signature) + app.connect("autodoc-process-docstring", process_docstring) diff --git a/docs/index.rst b/docs/index.rst index efaaf3a98..fe6e8e64f 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -24,6 +24,7 @@ Quick Start ------------ .. toctree:: :maxdepth: 2 + :caption: Quick Start tutorials/quickstart tutorials/otio-env-variables @@ -32,6 +33,7 @@ Tutorials ------------ .. toctree:: :maxdepth: 2 + :caption: Tutorials tutorials/adapters tutorials/architecture @@ -44,40 +46,40 @@ Tutorials tutorials/write-a-media-linker tutorials/write-a-hookscript tutorials/write-a-schemadef + tutorials/spatial-coordinates + tutorials/developing-a-new-schema tutorials/versioning-schemas Use Cases ------------ .. toctree:: :maxdepth: 2 + :caption: Use Cases use-cases/animation-shot-frame-ranges use-cases/conform-new-renders-into-cut use-cases/shots-added-removed-from-cut -API Reference -------------- +API References +-------------- -.. toctree:: - :maxdepth: 2 - - api/modules/opentimelineio - -C++ Implementation Reference ----------------------------- .. toctree:: - :maxdepth: 2 + :maxdepth: 3 + :caption: API References + + python_reference - cxx/bridges.md - cxx/cxx.md - cxx/older.md + cxx/bridges.md + cxx/cxx.md + cxx/older.md Schema Reference ---------------- .. toctree:: :maxdepth: 2 + :caption: Schema Reference tutorials/otio-file-format-specification tutorials/otio-serialized-schema @@ -88,9 +90,10 @@ Autogenerated Plugin Reference .. toctree:: :maxdepth: 2 + :caption: Plugins Reference tutorials/otio-plugins.md - + Indices and tables ------------------ diff --git a/docs/make.bat b/docs/make.bat index 4dbbf2887..13fd7f950 100644 --- a/docs/make.bat +++ b/docs/make.bat @@ -5,8 +5,8 @@ REM Command file for Sphinx documentation if "%SPHINXBUILD%" == "" ( set SPHINXBUILD=sphinx-build ) -set BUILDDIR=build -set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% source +set BUILDDIR=_build +set ALLSPHINXOPTS=-n -j8 -d %BUILDDIR%/doctrees %SPHINXOPTS% source set I18NSPHINXOPTS=%SPHINXOPTS% source if NOT "%PAPER%" == "" ( set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% diff --git a/docs/python_reference.rst b/docs/python_reference.rst new file mode 100644 index 000000000..4cc068b58 --- /dev/null +++ b/docs/python_reference.rst @@ -0,0 +1,8 @@ +Python +====== + +.. autosummary:: + :toctree: api/python + :recursive: + + opentimelineio diff --git a/docs/requirements.txt b/docs/requirements.txt new file mode 100644 index 000000000..b7ee962d5 --- /dev/null +++ b/docs/requirements.txt @@ -0,0 +1,4 @@ +sphinx==5.3.0 +readthedocs-sphinx-ext==2.1.9 # ?? +sphinx-rtd-theme +myst-parser==0.18.1 diff --git a/docs/tutorials/adapters.md b/docs/tutorials/adapters.md index cd11aa26c..5f9aa2a72 100644 --- a/docs/tutorials/adapters.md +++ b/docs/tutorials/adapters.md @@ -3,48 +3,47 @@ OpenTimelineIO supports, or plans to support, conversion adapters for many existing file formats. -### Final Cut Pro XML ### +## Final Cut Pro XML Final Cut 7 XML Format - Status: Supported via the `fcp_xml` adapter -- Reference +- [Reference](https://developer.apple.com/library/archive/documentation/AppleApplications/Reference/FinalCutPro_XML/AboutThisDoc/AboutThisDoc.html#//apple_ref/doc/uid/TP30001152-TPXREF101) Final Cut Pro X XML Format: - Status: Supported via the `fcpx_xml` adapter -- Intro to FCP X XML +- [Intro to FCP X XML](https://developer.apple.com/library/mac/documentation/FinalCutProX/Reference/FinalCutProXXMLFormat/Introduction/Introduction.html) -### Adobe Premiere Project ### +## Adobe Premiere Project - Based on guidance from Adobe, we support interchange with Adobe Premiere via the FCP 7 XML format (see above). -### CMX3600 EDL ### +## CMX3600 EDL - Status: Supported via the `cmx_3600` adapter - Includes support for ASC_CDL color correction metadata - Full specification: SMPTE 258M-2004 "For Television −− Transfer of Edit Decision Lists" - http://xmil.biz/EDL-X/CMX3600.pdf -- Reference +- [Reference](https://prohelp.apple.com/finalcutpro_help-r01/English/en/finalcutpro/usermanual/chapter_96_section_0.html) -### Avid AAF ### +## Avid AAF - Status: Reads and writes AAF compositions - includes clip, gaps, transitions but not markers or effects - - This adapter is still in progress, see the ongoing work here: AAF Project -- Spec -- Protocol + - This adapter is still in progress, see the ongoing work here: [AAF Project](https://github.com/AcademySoftwareFoundation/OpenTimelineIO/projects/1) +- [Spec](https://static.amwa.tv/ms-01-aaf-object-spec.pdf) +- [Protocol](https://static.amwa.tv/as-01-aaf-edit-protocol-spec.pdf) -- Depends on the `PyAAF2` module, so either: +- Depends on the [PyAAF2](https://github.com/markreidvfx/pyaaf2) module, so either: - `pip install pyaaf2` - ...or set `${OTIO_AAF_PYTHON_LIB}` to point the location of the PyAAF2 module -Contrib Adapters ----------------- +# Contrib Adapters The contrib area hosts adapters which come from the community (_not_ supported by the core-otio team) and may require extra dependencies. -### RV Session File ### +## RV Session File - Status: write-only adapter supported via the `rv_session` adapter. - need to set environment variables to locate `py-interp` and `rvSession.py` @@ -55,30 +54,30 @@ The contrib area hosts adapters which come from the community (_not_ supported - set `${OTIO_RV_PYTHON_LIB}` to point at the parent directory of `rvSession.py`: `setenv OTIO_RV_PYTHON_LIB /Applications/RV64.app/Contents/src/python` -### Maya Sequencer ### +## Maya Sequencer - Status: supported via the `maya_sequencer` adapter. - set `${OTIO_MAYA_PYTHON_BIN}` to point the location of `mayapy` within the maya installation. -### HLS Playlist ### +## HLS Playlist - Status: supported via the `hls_playlist` adapter. -### Avid Log Exchange (ALE) ### +## Avid Log Exchange (ALE) - Status: supported via the `ale` adapter. -### Text Burn-in Adapter ### +## Text Burn-in Adapter Uses FFmpeg to burn text overlays into video media. - Status: supported via the `burnins` adapter. -### GStreamer Editing Services Adapter ### +## GStreamer Editing Services Adapter - Status: supported via the `xges` adapter. -### Kdenlive Adapter ### +## Kdenlive Adapter - Status: supported via the kdenlive adapter diff --git a/docs/tutorials/architecture.md b/docs/tutorials/architecture.md index 497064823..ba1b6952c 100644 --- a/docs/tutorials/architecture.md +++ b/docs/tutorials/architecture.md @@ -109,7 +109,7 @@ A range in time. Encodes the start time and the duration, meaning that end_time OpenTimelineIO includes several adapters for reading and writing from other file formats. The `otio.adapters` module has convenience functions that will auto-detect which adapter to use, or you can specify the one you want. -Adapters can be added to the system (outside of the distribution) via JSON files that can be placed on the `OTIO_PLUGIN_MANIFEST_PATH` environment variable to be made available to OTIO. +Adapters can be added to the system (outside of the distribution) via JSON files that can be placed on the {term}`OTIO_PLUGIN_MANIFEST_PATH` environment variable to be made available to OTIO. Most common usage only cares about: - `timeline = otio.adapters.read_from_file(filepath)` @@ -121,17 +121,17 @@ The native format serialization (`.otio` files) is handled via the "otio_json" a In most cases you don't need to worry about adapter names, just use `otio.adapters.read_from_file()` and `otio.adapters.write_to_file` and it will figure out which one to use based on the filename extension. -For more information, see How To Write An OpenTimelineIO Adapter +For more information, see [How To Write An OpenTimelineIO Adapter](write-an-adapter). ## otio.media_linkers Media linkers run on the otio file after an adapter calls `.read_from_file()` or `.read_from_string()`. They are intended to replace media references that exist after the adapter runs (which depending on the adapter are likely to be `MissingReference`) with ones that point to valid files in the local system. Since media linkers are plugins, they can use proprietary knowledge or context and do not need to be part of OTIO itself. -You may also specify a media linker to be run after the adapter, either via the `media_linker_name` argument to `.read_from_file()` or `.read_from_string()` or via the `OTIO_DEFAULT_MEDIA_LINKER` environment variable. You can also turn the media linker off completely by setting the `media_linker_name` argument to `otio.media_linker.MediaLinkingPolicy.DoNotLinkMedia`. +You may also specify a media linker to be run after the adapter, either via the `media_linker_name` argument to `.read_from_file()` or `.read_from_string()` or via the {term}`OTIO_DEFAULT_MEDIA_LINKER` environment variable. You can also turn the media linker off completely by setting the `media_linker_name` argument to `otio.media_linker.MediaLinkingPolicy.DoNotLinkMedia`. -For more information about writing media linkers, see How To Write An OpenTimelineIO Media Linker +For more information about writing media linkers, see [How To Write An OpenTimelineIO Media Linker](write-a-media-linker). Example Scripts ---------------- -Example scripts are located in the examples subdirectory. +Example scripts are located in the [examples subdirectory](https://github.com/AcademySoftwareFoundation/OpenTimelineIO/tree/main/examples). diff --git a/docs/tutorials/contributing.md b/docs/tutorials/contributing.md index 4725c6dfc..dfc0e0c2e 100644 --- a/docs/tutorials/contributing.md +++ b/docs/tutorials/contributing.md @@ -5,12 +5,15 @@ We're excited to collaborate with the community and look forward to the many imp ## Contributor License Agreement -Before contributing code to OpenTimelineIO, we ask that you sign a Contributor License Agreement (CLA). At the root of the repo you can find the two possible CLAs: +Before contributing code to OpenTimelineIO, we ask that you sign a Contributor License Agreement (CLA). +When you create a pull request, the Linux Foundation's EasyCLA system will guide you through the process of signing the CLA. -* [OTIO_CLA_Corporate.pdf](https://github.com/PixarAnimationStudios/OpenTimelineIO/raw/main/OTIO_CLA_Corporate.pdf): please sign this one for corporate use -* [OTIO_CLA_Individual.pdf](https://github.com/PixarAnimationStudios/OpenTimelineIO/raw/main/OTIO_CLA_Individual.pdf): please sign this one if you're an individual contributor +If you are unable to use the EasyCLA system, you can send a signed CLA to `opentimelineio-tsc@aswf.io` (please make sure to include your github username) and wait for confirmation that we've received it. -Once your CLA is signed, send it to `opentimelineio-tsc@aswf.io` (please make sure to include your github username) and wait for confirmation that we've received it. After that, you can submit pull requests. +Here are the two possible CLAs: + +* [OTIO_CLA_Corporate.pdf](https://github.com/AcademySoftwareFoundation/OpenTimelineIO/raw/main/OTIO_CLA_Corporate.pdf): please sign this one for corporate use +* [OTIO_CLA_Individual.pdf](https://github.com/AcademySoftwareFoundation/OpenTimelineIO/raw/main/OTIO_CLA_Individual.pdf): please sign this one if you're an individual contributor ## Coding Conventions Please follow the coding convention and style in each file and in each library when adding new files. @@ -35,7 +38,7 @@ Add the primary OpenTimelineIO repo as upstream to make it easier to update your ```bash cd OpenTimelineIO -git remote add upstream https://github.com/PixarAnimationStudios/OpenTimelineIO.git +git remote add upstream https://github.com/AcademySoftwareFoundation/OpenTimelineIO.git ``` Now you fetch the latest changes from the OpenTimelineIO repo like this: diff --git a/docs/tutorials/developing-a-new-schema.md b/docs/tutorials/developing-a-new-schema.md new file mode 100644 index 000000000..7aa5c23b2 --- /dev/null +++ b/docs/tutorials/developing-a-new-schema.md @@ -0,0 +1,126 @@ +# Schema Proposal and Development Workflow + +## Introduction + +This document describes a process for proposing and developing a new schema for the +[OpenTimelineIO project](https://opentimeline.io). + +The process includes several steps: + +* Proposing at a TSC meeting and gathering interested parties for feedback + * Outlining example JSON +* Implementing and iterating on a branch +* Building support into an adapter as a demonstration +* Incrementing other schemas that are impacted (Eg. Changes to `Clip` to + implement `Media Multi Reference` + +## Examples + +A number of schemas have been proposed and introduced during +OpenTimelineIO's development. These include: + +* [ImageSequenceReference](https://github.com/AcademySoftwareFoundation/OpenTimelineIO/pull/602) +* [SpatialCoordinates](https://github.com/AcademySoftwareFoundation/OpenTimelineIO/pull/1219) +* [Multi media-reference](https://github.com/AcademySoftwareFoundation/OpenTimelineIO/pull/1241) + +## Core schema or Plugin? + +OpenTimelineIO has a number of plugin mechanisms, including the +[Schemadef](write-a-schemadef). Plugin schemadefs are great for things that +aren't expected to be useful to the broader community, or are specific to a particular studio, +workflow, or practice. Example of this might be a reference to a proprietary +database or a proprietary effect. They can also be a good place to prototype a +particular schema before proposing it to the community for adoption. + +## Proposal + +A proposal can be as fleshed out as a proposed implementation, or as vague as an +idea. Presenting the proposal at a Technical Steering Committee for discussion +is preferred so that interested parties can form a working group if necessary. +The goal of a TSC presentation would be to find view points / impacts that might not have been considered +and advertise the development to the community at large. + +Including an example JSON excerpt which has the fields you think might be needed +can help. + +References that are particularly helpful are examples from existing +applications/formats, information about how (or if) the schema participates in +temporal transformations, and other relevant citations. + +## Implementing and Iterating on a branch + +Development of schemas typically takes longer and includes more feedback and +review than normal development. To facilitate this, generally the project will +open a branch on the repository so that pull requests can be merged into the +prototype without disturbing the main branch. For example, the +[ImageSequenceReference](https://github.com/AcademySoftwareFoundation/OpenTimelineIO/pull/602) +branch demonstrates that workflow. + +A complete implementation should have a: + +* C++ core implementation in src/opentimelineio +* python binding in src/py-opentimelineio +* unit tests + +### Unit Tests + +Unit Tests should include a C++ test for the C++ component, a python test for +the python binding, and a baseline test. + +#### C++ test + +The C++ test should directly test the C++ interface. For examples of that, see +`tests/*.cpp`. + +#### Python tests + +The Python test should test the python binding, including any extra ergonomic +conveniences unique to the python implementation (iterators, etc). We use the +`unittest` python library. For examples of this, see: `tests/test_*.py`. + +#### Baseline tests + +Baseline tests are written in python and are intended to test the serializer. + +They include: + +* a file named `tests/baselines/empty_.json`, which is the result + of calling the constructor and then immediately serializing the object: + +```python +ys = YourSchema() +otio.adapters.write_to_file(ys, "empty_your_schema.json", adapter="otio_json") +``` + +* a test in `tests/test_json_backend.py` of the form: + +```python +class TestJsonFormat(unittest.TestCase, otio_test_utils.OTIOAssertions): + ... + def test_your_schema(self): + ys = YourSchema() + self.check_against_baseline(ys, "empty_your_schema") + ... +``` + +## Demo Adapter + +Providing an adapter that supports the schema can show how the schema is +translated into another format. For example, the +[ImageSequenceReference](https://github.com/AcademySoftwareFoundation/OpenTimelineIO/pull/722) +used the RV adapter to demonstrate how it could be used by an adapter. + +## Incrementing Other Schemas + +Depending on where the schema fits into the overall structure, other schemas +might need to be incremented or changed. For example, the Media +multi-reference caused the clip schema to increment. Considering and +implementing this is part of the implementation. Providing up and downgrade +functions ensure backwards and forwards compatibility. + +## Conclusion + +OpenTimelineIO is designed to evolve, and through its schema versioning system +hopes to adapt to changes in the world of timelines and time math. We hope +that working with and on OpenTimelineIO can be a positive, enriching experience +for the community. Thanks for being a part of it! diff --git a/docs/tutorials/feature-matrix.rst b/docs/tutorials/feature-matrix.rst index 1e31a657d..8239d97f5 100644 --- a/docs/tutorials/feature-matrix.rst +++ b/docs/tutorials/feature-matrix.rst @@ -14,7 +14,7 @@ Adapters may or may not support all of the features of OpenTimelineIO or the for +-------------------------+------+-------+--------+--------+-------+--------+-------+----------+ |Gap/Filler | ✔ | ✔ | ✔ | ✔ | ✔ | ✔ | ✖ | ✔ | +-------------------------+------+-------+--------+--------+-------+--------+-------+----------+ -|Markers | ✔ | ✔ | ✔ | ✔ | ✖ | N/A | ✖ | ✔ | +|Markers | ✔ | ✔ | ✔ | ✔ | ✔ | N/A | ✖ | ✔ | +-------------------------+------+-------+--------+--------+-------+--------+-------+----------+ |Nesting | ✔ | ✖ | ✔ | ✔ | ✔ | W-O | ✔ | ✔ | +-------------------------+------+-------+--------+--------+-------+--------+-------+----------+ diff --git a/docs/tutorials/otio-env-variables.md b/docs/tutorials/otio-env-variables.md index b20285a1a..759b7d898 100644 --- a/docs/tutorials/otio-env-variables.md +++ b/docs/tutorials/otio-env-variables.md @@ -5,15 +5,39 @@ various aspects of OTIO. ## Plugin Configuration -These variables must be set _before_ the OpenTimelineIO python library is imported. +These variables must be set _before_ the OpenTimelineIO python library is imported. They only impact the python library. The C++ library has no environment variables. -- `OTIO_PLUGIN_MANIFEST_PATH`: a ":" separated string with paths to .manifest.json files that contain OTIO plugin manifests. See: Tutorial on how to write an adapter plugin. -- `OTIO_DEFAULT_MEDIA_LINKER`: the name of the default media linker to use after reading a file, if "" then no media linker is automatically invoked. -- `OTIO_DISABLE_PKG_RESOURCE_PLUGINS`: By default, OTIO will use the pkg_resource entry_points mechanism to discover plugins that have been installed into the current python environment. pkg_resources, however, can be slow in certain cases, so for users who wish to disable this behavior, this variable can be set to 1. +```{glossary} + +OTIO_PLUGIN_MANIFEST_PATH + A colon (`:`) on POSIX system (or a semicolon (`;`) on Windows) separated string with paths + to `.manifest.json` files that contain OTIO plugin manifests. + See the [tutorial on how to write an adapter plugin](write-an-adapter.md) for additional details. + +OTIO_DEFAULT_MEDIA_LINKER + The name of the default media linker to use after reading a file, if `""` then no + media linker is automatically invoked. + +OTIO_DISABLE_ENTRYPOINTS_PLUGINS + By default, OTIO will use the `importlib.metadata` entry_points mechanism to discover plugins + that have been installed into the current python environment. For users who wish to disable this + behavior, this variable can be set to 1. + +OTIO_DEFAULT_TARGET_VERSION_FAMILY_LABEL + If no downgrade arguments are passed to `write_to_file`/`write_to_string`, use the downgrade manifest + specified by the family/label combination in the variable. Variable is of the form `FAMILY:LABEL`. + Only one tuple of `FAMILY:LABEL` may be specified. +``` ## Unit tests These variables only impact unit tests. -- `OTIO_DISABLE_SHELLOUT_TESTS`: When running the unit tests, skip the console tests that run the otiocat program and check output through the shell. This is desirable in environments where running the commandline tests is not meaningful or problematic. Does not disable the tests that run through python calling mechanisms. -- `OTIO_DISABLE_SERIALIZED_SCHEMA_TEST`: Skip the unit tests that generate documentation and compare the current state of the schema against the stored one. Useful if the documentation is not available from the test directory. +```{glossary} + +OTIO_DISABLE_SHELLOUT_TESTS + When running the unit tests, skip the console tests that run the otiocat program and check output through the shell. This is desirable in environments where running the commandline tests is not meaningful or problematic. Does not disable the tests that run through python calling mechanisms. + +OTIO_DISABLE_SERIALIZED_SCHEMA_TEST + Skip the unit tests that generate documentation and compare the current state of the schema against the stored one. Useful if the documentation is not available from the test directory. +``` diff --git a/docs/tutorials/otio-file-format-specification.md b/docs/tutorials/otio-file-format-specification.md index 4a8192e00..9d284fb9b 100644 --- a/docs/tutorials/otio-file-format-specification.md +++ b/docs/tutorials/otio-file-format-specification.md @@ -83,7 +83,7 @@ the core OTIO schema. Due to the fact that many different workflows can and will use metadata, it is important to group metadata inside namespaces so that independent workflows can coexist without encountering name collisions. In the example below, there is metadata on the Timeline and on several Clips for both a hypothetical `my_playback_tool` and `my_production_tracking_system` that could coexist with anything else added under a different namespace. -Metadata can also be useful when prototyping new OTIO schemas. An existing object can be extended with metadata which can later be migrated into a new schema version, or a custom schema defined in a SchemaDef plugin. +Metadata can also be useful when prototyping new OTIO schemas. An existing object can be extended with metadata which can later be migrated into a new schema version, or a custom schema defined in a [SchemaDef plugin](write-a-schemadef). ## Example: @@ -273,4 +273,4 @@ Metadata can also be useful when prototyping new OTIO schemas. An existing objec ## Schema Specification -To see an autogenerated documentation of the serialized types and their fields, see this: Autogenerated Serialized File Format +To see an autogenerated documentation of the serialized types and their fields, see this: [Autogenerated Serialized File Format](otio-serialized-schema). diff --git a/docs/tutorials/otio-filebundles.md b/docs/tutorials/otio-filebundles.md index da9bd367c..02b505b99 100644 --- a/docs/tutorials/otio-filebundles.md +++ b/docs/tutorials/otio-filebundles.md @@ -27,13 +27,15 @@ The file bundle adapters expect the `target_url` field of the `media_reference` File bundles, regardless of how they're encoded, have a consistent structure: -- something.otioz (or .otiod) - - content.otio - - version.txt - - media - - media1 - - media2 - - media3 +``` +something.otioz +├── content.otio +├── version +└── media + ├── media1 +    ├── media2 +    └── media3 +``` ### content.otio file diff --git a/docs/tutorials/otio-plugins.md b/docs/tutorials/otio-plugins.md index eb2dc917c..924de10d8 100644 --- a/docs/tutorials/otio-plugins.md +++ b/docs/tutorials/otio-plugins.md @@ -28,11 +28,9 @@ Manifest path: `opentimelineio/adapters/builtin_adapters.plugin_manifest.json` Adapter plugins convert to and from OpenTimelineIO. - Adapters documentation page for more -information +[Adapters documentation page for more information](./adapters). -Tutorial on how to write an -adapter. +[Tutorial on how to write an adapter](write-an-adapter). ### cmx_3600 @@ -108,7 +106,7 @@ OpenTimelineIO Final Cut Pro 7 XML Adapter. ### otio_json ``` -This adapter lets you read and write native .otio files +Adapter for reading and writing native .otio json files. ``` *source*: `opentimelineio/adapters/otio_json.py` @@ -149,14 +147,23 @@ Serializes an OpenTimelineIO object into a file indent (int): number of spaces for each json indentation level. Use -1 for no indentation or newlines. + If target_schema_versions is None and the environment variable + "OTIO_DEFAULT_TARGET_VERSION_FAMILY_LABEL" is set, will read a map out of + that for downgrade target. The variable should be of the form + FAMILY:LABEL, for example "MYSTUDIO:JUNE2022". + Returns: bool: Write success Raises: ValueError: on write error + otio.exceptions.InvalidEnvironmentVariableError: if there is a problem + with the default environment variable + "OTIO_DEFAULT_TARGET_VERSION_FAMILY_LABEL". ``` - input_otio - filepath + - target_schema_versions - indent - write_to_string: ``` @@ -167,10 +174,21 @@ Serializes an OpenTimelineIO object into a string indent (int): number of spaces for each json indentation level. Use -1 for no indentation or newlines. + If target_schema_versions is None and the environment variable + "OTIO_DEFAULT_TARGET_VERSION_FAMILY_LABEL" is set, will read a map out of + that for downgrade target. The variable should be of the form + FAMILY:LABEL, for example "MYSTUDIO:JUNE2022". + Returns: str: A json serialized string representation + + Raises: + otio.exceptions.InvalidEnvironmentVariableError: if there is a problem + with the default environment variable + "OTIO_DEFAULT_TARGET_VERSION_FAMILY_LABEL". ``` - input_otio + - target_schema_versions - indent @@ -269,8 +287,7 @@ Points in SVG are y-down. Media Linkers run after the adapter has read in the file and convert the media references into valid references where appropriate. - Tutorial on how to write a -Media Linker +[Tutorial on how to write a Media Linker](write-a-media-linker). @@ -278,8 +295,7 @@ Media Linker SchemaDef plugins define new external schema. - Tutorial on how to write a -schemadef +[Tutorial on how to write a schemadef](write-a-schemadef). @@ -287,8 +303,7 @@ schemadef HookScripts are extra plugins that run on _hooks_. -Tutorial on how to write a -hookscript. +[Tutorial on how to write a hookscript](write-a-hookscript). @@ -312,11 +327,9 @@ Manifest path: `opentimelineio_contrib/adapters/contrib_adapters.plugin_manifest Adapter plugins convert to and from OpenTimelineIO. - Adapters documentation page for more -information +[Adapters documentation page for more information](./adapters). -Tutorial on how to write an -adapter. +[Tutorial on how to write an adapter](write-an-adapter). ### AAF @@ -567,38 +580,6 @@ Adapter entry point for writing. -### kdenlive - -``` -Kdenlive (MLT XML) Adapter. -``` - -*source*: `opentimelineio_contrib/adapters/kdenlive.py` - - -*Supported Features (with arguments)*: - -- read_from_string: -``` -Read a Kdenlive project (MLT XML) - Kdenlive uses a given MLT project layout, similar to Shotcut, - combining a "main_bin" playlist to organize source media, - and a "global_feed" tractor for timeline. - (in Kdenlive 19.x, timeline tracks include virtual sub-track, unused for now) -``` - - input_str -- write_to_string: -``` -Write a timeline to Kdenlive project - Re-creating the bin storing all used source clips - and constructing the tracks -``` - - input_otio - - - - - ### maya_sequencer ``` @@ -620,25 +601,6 @@ Maya Sequencer Adapter Harness -### rv_session - -``` -RvSession Adapter harness -``` - -*source*: `opentimelineio_contrib/adapters/rv.py` - - -*Supported Features (with arguments)*: - -- write_to_file: - - input_otio - - filepath - - - - - ### xges ``` @@ -682,8 +644,7 @@ Necessary write method for otio adapter Media Linkers run after the adapter has read in the file and convert the media references into valid references where appropriate. - Tutorial on how to write a -Media Linker +[Tutorial on how to write a Media Linker](write-a-media-linker). @@ -691,8 +652,7 @@ Media Linker SchemaDef plugins define new external schema. - Tutorial on how to write a -schemadef +[Tutorial on how to write a schemadef](write-a-schemadef). ### xges @@ -790,8 +750,7 @@ An OpenTimelineIO Schema for storing a GESTrack. HookScripts are extra plugins that run on _hooks_. -Tutorial on how to write a -hookscript. +[Tutorial on how to write a hookscript](write-a-hookscript). diff --git a/docs/tutorials/otio-serialized-schema-only-fields.md b/docs/tutorials/otio-serialized-schema-only-fields.md index 624f96aaf..6c70a0bf0 100644 --- a/docs/tutorials/otio-serialized-schema-only-fields.md +++ b/docs/tutorials/otio-serialized-schema-only-fields.md @@ -25,7 +25,6 @@ changes. If it needs to be updated and this file regenerated, run: ### Adapter.1 parameters: -- *execution_scope* - *filepath* - *name* - *suffixes* @@ -77,7 +76,6 @@ parameters: ### HookScript.1 parameters: -- *execution_scope* - *filepath* - *name* @@ -86,7 +84,6 @@ parameters: ### MediaLinker.1 parameters: -- *execution_scope* - *filepath* - *name* @@ -121,11 +118,11 @@ parameters: - *hooks* - *media_linkers* - *schemadefs* +- *version_manifests* ### SerializableObject.1 parameters: -- *execution_scope* - *filepath* - *name* @@ -281,6 +278,5 @@ parameters: ### SchemaDef.1 parameters: -- *execution_scope* - *filepath* - *name* diff --git a/docs/tutorials/otio-serialized-schema.md b/docs/tutorials/otio-serialized-schema.md index 4206b6bf3..d109d14fd 100644 --- a/docs/tutorials/otio-serialized-schema.md +++ b/docs/tutorials/otio-serialized-schema.md @@ -1,7 +1,8 @@ # Serialized Data Documentation This documents all the OpenTimelineIO classes that serialize to and from JSON, -omitting SchemaDef plugins. This document is automatically generated by running: +omitting SchemaDef plugins. This document is automatically generated by +running: `src/py-opentimelineio/opentimelineio/console/autogen_serialized_datamodel.py` @@ -28,9 +29,11 @@ changes. If it needs to be updated and this file regenerated, run: ``` Adapters convert between OTIO and other formats. - Note that this class is not subclassed by adapters. Rather, an adapter is + Note that this class is not subclassed by adapters. Rather, an adapter is a python module that implements at least one of the following functions: + .. code-block:: python + write_to_string(input_otio) write_to_file(input_otio, filepath) (optionally inferred) read_from_string(input_str) @@ -41,13 +44,12 @@ Adapters convert between OTIO and other formats. to OTIO. You should not need to extend this class to create new adapters for OTIO. - For more information: - https://opentimelineio.readthedocs.io/en/latest/tutorials/write-an-adapter.html# # noqa + For more information: https://opentimelineio.readthedocs.io/en/latest/tutorials/write-an- +adapter.html. # noqa ``` parameters: -- *execution_scope*: Describes whether this adapter is executed in the current python process or in a subshell. Options are: ['in process', 'out of process']. - *filepath*: Absolute path or relative path to adapter module from location of json. - *name*: Adapter name. - *suffixes*: File suffixes associated with this adapter. @@ -61,7 +63,10 @@ parameters: *documentation*: ``` -None + +An object that can be composed within a :class:`~Composition` (such as :class:`~Track` or +:class:`.Stack`). + ``` parameters: @@ -75,12 +80,16 @@ parameters: *documentation*: ``` -None + +Base class for an :class:`~Item` that contains :class:`~Composable`\s. + +Should be subclassed (for example by :class:`.Track` and :class:`.Stack`), not used directly. + ``` parameters: - *effects*: -- *enabled*: If true, an Item contributes to compositions. Analogous to Mute in various NLEs. +- *enabled*: If true, an Item contributes to compositions. For example, when an audio/video clip is ``enabled=false`` the clip is muted/hidden. - *markers*: - *metadata*: - *name*: @@ -98,7 +107,7 @@ None parameters: - *effects*: -- *enabled*: If true, an Item contributes to compositions. Analogous to Mute in various NLEs. +- *enabled*: If true, an Item contributes to compositions. For example, when an audio/video clip is ``enabled=false`` the clip is muted/hidden. - *markers*: - *metadata*: - *name*: @@ -147,7 +156,6 @@ None ``` parameters: -- *execution_scope*: Describes whether this adapter is executed in the current python process or in a subshell. Options are: ['in process', 'out of process']. - *filepath*: Absolute path or relative path to adapter module from location of json. - *name*: Adapter name. @@ -164,7 +172,6 @@ None ``` parameters: -- *execution_scope*: Describes whether this adapter is executed in the current python process or in a subshell. Options are: ['in process', 'out of process']. - *filepath*: Absolute path or relative path to adapter module from location of json. - *name*: Adapter name. @@ -177,7 +184,10 @@ parameters: *documentation*: ``` -None + +The RationalTime class represents a measure of time of :math:`rt.value/rt.rate` seconds. +It can be rescaled into another :class:`~RationalTime`'s rate. + ``` parameters: @@ -191,7 +201,11 @@ parameters: *documentation*: ``` -None + +The TimeRange class represents a range in time. It encodes the start time and the duration, +meaning that :meth:`end_time_inclusive` (last portion of a sample in the time range) and +:meth:`end_time_exclusive` can be computed. + ``` parameters: @@ -205,7 +219,7 @@ parameters: *documentation*: ``` -None +1D transform for :class:`~RationalTime`. Has offset and scale. ``` parameters: @@ -240,6 +254,7 @@ parameters: - *hooks*: Hooks that hooks scripts can be attached to. - *media_linkers*: Media Linkers this manifest describes. - *schemadefs*: Schemadefs this manifest describes. +- *version_manifests*: Sets of versions to downgrade schemas to. ### SerializableObject.1 @@ -254,7 +269,6 @@ A class of plugin that is encoded in a python module, exposed via a ``` parameters: -- *execution_scope*: Describes whether this adapter is executed in the current python process or in a subshell. Options are: ['in process', 'out of process']. - *filepath*: Absolute path or relative path to adapter module from location of json. - *name*: Adapter name. @@ -267,13 +281,17 @@ parameters: *documentation*: ``` -None + +A :class:`~Clip` is a segment of editable media (usually audio or video). + +Contains a :class:`.MediaReference` and a trim on that media reference. + ``` parameters: - *active_media_reference_key*: - *effects*: -- *enabled*: If true, an Item contributes to compositions. Analogous to Mute in various NLEs. +- *enabled*: If true, an Item contributes to compositions. For example, when an audio/video clip is ``enabled=false`` the clip is muted/hidden. - *markers*: - *media_references*: - *metadata*: @@ -319,14 +337,18 @@ parameters: *documentation*: ``` -None +Hold the first frame of the clip for the duration of the clip. ``` parameters: - *effect_name*: - *metadata*: - *name*: -- *time_scalar*: +- *time_scalar*: Linear time scalar applied to clip. 2.0 means the clip occupies half the time in the parent item, i.e. plays at double speed, +0.5 means the clip occupies twice the time in the parent item, i.e. plays at half speed. + +Note that adjusting the time_scalar of a :class:`~LinearTimeWarp` does not affect the duration of the item this effect is attached to. +Instead it affects the speed of the media displayed within that item. ### Gap.1 @@ -340,7 +362,7 @@ None parameters: - *effects*: -- *enabled*: If true, an Item contributes to compositions. Analogous to Mute in various NLEs. +- *enabled*: If true, an Item contributes to compositions. For example, when an audio/video clip is ``enabled=false`` the clip is muted/hidden. - *markers*: - *metadata*: - *name*: @@ -372,13 +394,23 @@ parameters: ``` -An ImageSequenceReference refers to a numbered series of single-frame image files. Each file can be referred to by a URL generated by the ImageSequenceReference. +An ImageSequenceReference refers to a numbered series of single-frame image files. Each file can be +referred to by a URL generated by the :class:`~ImageSequenceReference`. + +Image sequences can have URLs with discontinuous frame numbers, for instance if you've only rendered + every other frame in a sequence, your frame numbers may be 1, 3, 5, etc. This is configured using +the ``frame_step`` attribute. In this case, the 0th image in the sequence is frame 1 and the 1st +image in the sequence is frame 3. Because of this there are two numbering concepts in the image +sequence, the image number and the frame number. -Image sequncences can have URLs with discontinuous frame numbers, for instance if you've only rendered every other frame in a sequence, your frame numbers may be 1, 3, 5, etc. This is configured using the ``frame_step`` attribute. In this case, the 0th image in the sequence is frame 1 and the 1st image in the sequence is frame 3. Because of this there are two numbering concepts in the image sequence, the image number and the frame number. +Frame numbers are the integer numbers used in the frame file name. Image numbers are the 0-index +based numbers of the frames available in the reference. Frame numbers can be discontinuous, image +numbers will always be zero to the total count of frames minus 1. -Frame numbers are the integer numbers used in the frame file name. Image numbers are the 0-index based numbers of the frames available in the reference. Frame numbers can be discontinuous, image numbers will always be zero to the total count of frames minus 1. +An example for 24fps media with a sample provided each frame numbered 1-1000 with a path +``/show/sequence/shot/sample_image_sequence.%04d.exr`` might be -An example for 24fps media with a sample provided each frame numbered 1-1000 with a path ``/show/sequence/shot/sample_image_sequence.%04d.exr`` might be:: +.. code-block:: json { "available_range": { @@ -400,7 +432,9 @@ An example for 24fps media with a sample provided each frame numbered 1-1000 wit "frame_zero_padding": 4, } -The same duration sequence but with only every 2nd frame available in the sequence would be:: +The same duration sequence but with only every 2nd frame available in the sequence would be + +.. code-block:: json { "available_range": { @@ -422,11 +456,15 @@ The same duration sequence but with only every 2nd frame available in the sequen "frame_zero_padding": 4, } -A list of all the frame URLs in the sequence can be generated, regardless of frame step, with the following list comprehension:: +A list of all the frame URLs in the sequence can be generated, regardless of frame step, with the +following list comprehension + +.. code-block:: python [ref.target_url_for_image_number(i) for i in range(ref.number_of_images_in_sequence())] -Negative ``start_frame`` is also handled. The above example with a ``start_frame`` of ``-1`` would yield the first three target urls as: +Negative ``start_frame`` is also handled. The above example with a ``start_frame`` of ``-1`` would +yield the first three target urls as: - ``file:///show/sequence/shot/sample_image_sequence.-0001.exr`` - ``file:///show/sequence/shot/sample_image_sequence.0000.exr`` @@ -440,7 +478,7 @@ parameters: - *frame_step*: Step between frame numbers in file names. - *frame_zero_padding*: Number of digits to pad zeros out to in frame numbers. - *metadata*: -- *missing_frame_policy*: Enum ``ImageSequenceReference.MissingFramePolicy`` directive for how frames in sequence not found on disk should be handled. +- *missing_frame_policy*: Directive for how frames in sequence not found during playback or rendering should be handled. - *name*: - *name_prefix*: Everything in the file name leading up to the frame number. - *name_suffix*: Everything after the frame number in the file name. @@ -455,14 +493,20 @@ parameters: *documentation*: ``` -None + +A time warp that applies a linear speed up or slow down across the entire clip. + ``` parameters: - *effect_name*: - *metadata*: - *name*: -- *time_scalar*: +- *time_scalar*: Linear time scalar applied to clip. 2.0 means the clip occupies half the time in the parent item, i.e. plays at double speed, +0.5 means the clip occupies twice the time in the parent item, i.e. plays at half speed. + +Note that adjusting the time_scalar of a :class:`~LinearTimeWarp` does not affect the duration of the item this effect is attached to. +Instead it affects the speed of the media displayed within that item. ### Marker.2 @@ -471,12 +515,18 @@ parameters: *documentation*: ``` -None + +A marker indicates a marked range of time on an item in a timeline, usually with a name, color or +other metadata. + +The marked range may have a zero duration. The marked range is in the owning item's time coordinate +system. + ``` parameters: -- *color*: -- *marked_range*: +- *color*: Color string for this marker (for example: 'RED'), based on the :class:`~Color` enum. +- *marked_range*: Range this marker applies to, relative to the :class:`.Item` this marker is attached to (e.g. the :class:`.Clip` or :class:`.Track` that owns this marker). - *metadata*: - *name*: @@ -487,7 +537,12 @@ parameters: *documentation*: ``` -None + +Represents media for which a concrete reference is missing. + +Note that a :class:`~MissingReference` may have useful metadata, even if the location of the media +is not known. + ``` parameters: @@ -503,7 +558,18 @@ parameters: *documentation*: ``` -None + +A container which can hold an ordered list of any serializable objects. Note that this is not a +:class:`.Composition` nor is it :class:`.Composable`. + +This container approximates the concept of a bin - a collection of :class:`.SerializableObject`\s +that do +not have any compositional meaning, but can serialize to/from OTIO correctly, with metadata and +a named collection. + +A :class:`~SerializableCollection` is useful for serializing multiple timelines, clips, or media +references to a single file. + ``` parameters: @@ -522,7 +588,7 @@ None parameters: - *effects*: -- *enabled*: If true, an Item contributes to compositions. Analogous to Mute in various NLEs. +- *enabled*: If true, an Item contributes to compositions. For example, when an audio/video clip is ``enabled=false`` the clip is muted/hidden. - *markers*: - *metadata*: - *name*: @@ -535,7 +601,7 @@ parameters: *documentation*: ``` -None +Base class for all effects that alter the timing of an item. ``` parameters: @@ -571,7 +637,7 @@ None parameters: - *effects*: -- *enabled*: If true, an Item contributes to compositions. Analogous to Mute in various NLEs. +- *enabled*: If true, an Item contributes to compositions. For example, when an audio/video clip is ``enabled=false`` the clip is muted/hidden. - *kind*: - *markers*: - *metadata*: @@ -585,15 +651,16 @@ parameters: *documentation*: ``` -None +Represents a transition between the two adjacent items in a :class:`.Track`. For example, a cross +dissolve or wipe. ``` parameters: -- *in_offset*: +- *in_offset*: Amount of the previous clip this transition overlaps, exclusive. - *metadata*: - *name*: -- *out_offset*: -- *transition_type*: +- *out_offset*: Amount of the next clip this transition overlaps, exclusive. +- *transition_type*: Kind of transition, as defined by the :class:`Type` enum. ### SchemaDef.1 @@ -606,6 +673,5 @@ None ``` parameters: -- *execution_scope*: Describes whether this adapter is executed in the current python process or in a subshell. Options are: ['in process', 'out of process']. - *filepath*: Absolute path or relative path to adapter module from location of json. - *name*: Adapter name. diff --git a/docs/tutorials/quickstart.md b/docs/tutorials/quickstart.md index d6dbd91ac..283513b59 100644 --- a/docs/tutorials/quickstart.md +++ b/docs/tutorials/quickstart.md @@ -9,9 +9,7 @@ This is for users who wish to get started using the "OTIOView" application to in OTIOView has an additional prerequisite to OTIO: - Try `python -m pip install PySide2` or `python -m pip install PySide6` -- If that doesn't work, try downloading PySide here: https://wiki.qt.io/Qt_for_Python - -You probably want the prebuilt binary for your platform. PySide generally includes a link to the appropriate version of Qt as well. +- If difficulties are encountered, please file an issue on OpenTimelineIO's github for assistance. ## Install OTIO @@ -19,8 +17,9 @@ You probably want the prebuilt binary for your platform. PySide generally inclu ## Configure Environment Variables for extra adapters -By default, when you install OTIO you will only get the "Core" adapters, which include CMX EDL, Final Cut Pro 7 XML, and the built in JSON format. In order to get access to the "contrib" adapters (which includes the maya sequencer, rv and others) you'll need to set some environment variables. If you need support for these formats, please consult the - Adapters documentation page for details +A default OTIO installation includes only the "Core" adapters, which include CMX EDL, Final Cut Pro 7 XML, and the built in JSON format. +In order to get access to the "contrib" adapters (which includes the maya sequencer, rv and others), please consult the +[Adapters documentation page for details](./adapters). ## Run OTIOView @@ -31,7 +30,7 @@ Once you have pip installed OpenTimelineIO, you should be able to run: # Developer Quickstart Get the source and submodules: -+ `git clone git@github.com:PixarAnimationStudios/OpenTimelineIO.git` ++ `git clone git@github.com:AcademySoftwareFoundation/OpenTimelineIO.git` Before reading further, it is good to note that there is two parts to the C++ code: the OTIO C++ library that you can use in your C++ projects, @@ -63,7 +62,7 @@ with OpenTimelineIO due to spaces in the path. ## To build OTIO for Python development: -+ `python -m pip install .` ++ `python -m pip install -e .` ## To build OTIO for both C++ and Python development: @@ -106,7 +105,7 @@ To use opentime without opentimelineio, link with -lopentime instead, and compil # Debugging Quickstart -### Linux / GDB / LLDB +## Linux / GDB / LLDB To compile in debug mode, set the `OTIO_CXX_DEBUG_BUILD` environment variable to any value and then `python -m pip install`. @@ -129,9 +128,9 @@ One handy tip is that you can trigger a breakpoint in gdb by inserting a SIGINT: GDB will automatically break when it hits the SIGINT line. -## How to Generate the C++ Documentation: +# How to Generate the C++ Documentation: -### Mac / Linux +## Mac / Linux The doxygen docs can be generated with the following commands: diff --git a/docs/tutorials/versioning-schemas.md b/docs/tutorials/versioning-schemas.md index 3adea62f4..33d9efb6c 100644 --- a/docs/tutorials/versioning-schemas.md +++ b/docs/tutorials/versioning-schemas.md @@ -1,53 +1,290 @@ # Versioning Schemas +## Overview -During development, it is natural that the fields on objects in OTIO change. To accommodate this, OTIO has a system for handling version differences and upgrading older schemas to new ones. There are two components: +This document describes OpenTimelineIO's systems for dealing with different schema versions when reading files, writing files, or during development of the library itself. It is intended for developers who are integrating OpenTimelineIO into their pipelines or applications, or working directly on OpenTimelineIO. -1. `serializeable_label` on the class has a name and version field: `Foo.5` -- `Foo` is the schema name and `5` is the version. -2. `upgrade_function_for` decorator +TL;DR for users: OpenTimelineIO should be able to read files produced by older versions of the library and be able to write files that are compatible with older versions of the library from newer versions. +## Schema/Version Introduction -Changing a Field ---------------------- +Each SerializableObject (the base class of OpenTimelineIO) has `schema_name` and `schema_version` fields. The `schema_name` is a string naming the schema, for example, `Clip`, and the `schema_version` is an integer of the current version number, for example, `3`. -For example, lets say you have class: +SerializableObjects can be queried for these using the `.schema_name()` and `.schema_version()` methods. For a given release of the OpenTimelineIO library, in-memory objects the library creates will always be the same schema version. In other words, if `otio.schema.Clip()` instantiates an object with `schema_version` 2, there is no way to get an in-memory `Clip` object with version 1. + +OpenTimelineIO can still interoperate with older and newer versions of the library by way of the schema upgrading/downgrading system. As OpenTimelineIO deserializes json from a string or disk, it will upgrade the schemas to the version supported by the library before instantiating the concrete in-memory object. Similarly, when serializing OpenTimelineIO back to disk, the user can instruct OpenTimelineIO to downgrade the JSON to older versions of the schemas. In this way, a newer version of OpenTimelineIO can read files with older schemas, and a newer version of OpenTimelineIO can generate JSON with older schemas in it. + +## Schema Upgrading + +Once a type is registered to OpenTimelineIO, developers may also register upgrade functions. In python, each upgrade function takes a dictionary and returns a dictionary. In C++, the AnyDictionary is manipulated in place. Each upgrade function is associated with a version number - this is the version number that it upgrades to. + +C++ Example (can be viewed/run in `examples/upgrade_downgrade_example.cpp`): + + +```cpp +class SimpleClass : public otio::SerializableObject +{ +public: + struct Schema + { + static auto constexpr name = "SimpleClass"; + static int constexpr version = 2; + }; + + void set_new_field(int64_t val) { _new_field = val; } + int64_t new_field() const { return _new_field; } + +protected: + using Parent = SerializableObject; + + virtual ~SimpleClass() = default; + + virtual bool + read_from(Reader& reader) + { + auto result = ( + reader.read("new_field", &_new_field) + && Parent::read_from(reader) + ); + + return result; + } + + virtual void + write_to(Writer& writer) const + { + Parent::write_to(writer); + writer.write("new_field", _new_field); + } + +private: + int64_t _new_field; +}; + + // later, during execution: + + // register type and upgrade/downgrade functions + otio::TypeRegistry::instance().register_type(); + + // 1->2 + otio::TypeRegistry::instance().register_upgrade_function( + SimpleClass::Schema::name, + 2, + [](otio::AnyDictionary* d) + { + (*d)["new_field"] = (*d)["my_field"]; + d->erase("my_field"); + } + ); +``` + +Python Example: + +```python +@otio.core.register_type +class SimpleClass(otio.core.SerializableObject): + serializable_label = "SimpleClass.2" + my_field = otio.core.serializable_field("new_field", int) + +@otio.core.upgrade_function_for(SimpleClass, 2) +def upgrade_one_to_two(data): + return {"new_field" : data["my_field"] } +``` + +When upgrading schemas, OpenTimelineIO will call each upgrade function in order in an attempt to get to the current version. For example, if a schema is registered to have version 3, and a file with version 1 is read, OpenTimelineIO will attempt to call the 1->2 function, then the 2->3 function before instantiating the concrete class. + +## Schema Downgrading + +Similarly, once a type is registered, downgrade functions may be registered. Downgrade functions take a dictionary of the version specified and return a dictionary of the schema version one lower. For example, if a downgrade function is registered for version 5, that will downgrade from 5 to 4. + +C++ Example, building off the prior section SimpleClass example (can be viewed/run in `examples/upgrade_downgrade_example.cpp`): + + +```cpp +// 2->1 +otio::TypeRegistry::instance().register_downgrade_function( + SimpleClass::Schema::name, + 2, + [](otio::AnyDictionary* d) + { + (*d)["my_field"] = (*d)["new_field"]; + d->erase("new_field"); + } +); +``` + +Python Example: + +```python +@otio.core.upgrade_function_for(SimpleClass, 2) +def downgrade_two_to_one(data): + return {"my_field" : data["new_field"] } +``` + +To specify what version of a schema to downgrade to, the serialization functions include an optional `schema_version_targets` argument which is a map of schema name to target schema version. During serialization, any schemas who are listed in the map and are of greater version than specified in the map will be converted to AnyDictionary and run through the necessary downgrade functions before being serialized. + +Example C++: + +```cpp +auto sc = otio::SerializableObject::Retainer(new SimpleClass()); +sc->set_new_field(12); + +// this will only downgrade the SimpleClass, to version 1 +otio::schema_version_map downgrade_manifest = { + {"SimpleClass", 1} +}; + +// write it out to disk, downgrading to version 1 +sc->to_json_file("/var/tmp/simpleclass.otio", &err, &downgrade_manifest); +``` + +Example python: + +```python +sc = SimpleClass() +otio.adapters.write_to_file( + sc, + "/path/to/output.otio", + target_schema_versions={"SimpleClass":1} +) +``` + +### Schema-Version Sets + +In addition to passing in dictionaries of desired target schema versions, OpenTimelineIO also provides some tools for having sets of schemas with an associated label. The core C++ library contains a compiled-in map of them, the `CORE_VERSION_MAP`. This is organized (as of v0.15.0) by library release versions label, ie "0.15.0", "0.14.0" and so on. + +In order to downgrade to version 0.15.0 for example: + +```cpp +auto downgrade_manifest = otio::CORE_VERSION_MAP["0.15.0"]; + +// write it out to disk, downgrading to version 1 +sc->to_json_file("/var/tmp/simpleclass.otio", &err, &downgrade_manifest); +``` + +In python, an additional level of indirection is provided, "FAMILY", which is intended to allow developers to define their own sets of target versions for their plugin schemas. For example, a studio might have a family named "MYFAMILY" under which they organize labels for their internal releases of their own plugins. + +These can be defined in a plugin manifest, which is a `.plugin_manifest.json` file found on the environment variable {term}`OTIO_PLUGIN_MANIFEST_PATH`. + +For example: + +```python +{ + "OTIO_SCHEMA" : "PluginManifest.1", + "version_manifests": { + "MYFAMILY": { + "June2022": { + "SimpleClass": 2, + ... + }, + "May2022": { + "SimpleClass": 1, + ... + } + } + } +} +``` + +To fetch the version maps and work with this, the python API provides some additional functions: + +```python +# example using a built in family +downgrade_manifest = otio.versioning.fetch_map("OTIO_CORE", "0.15.0") +otio.adapters.write_to_file( + sc, + "/path/to/file.otio", + target_schema_versions=downgrade_manifest +) + +# using a custom family defined in a plugin manifest json file +downgrade_manifest = otio.versioning.fetch_map("MYFAMILY", "June2022") +otio.adapters.write_to_file( + sc, + "/path/to/file.otio", + target_schema_versions=downgrade_manifest +) + +``` + +To fetch the version sets defined by the core from python, use the `OTIO_CORE` family of version sets. + +See the [versioning module](../api/python/opentimelineio.versioning.rst) for more information on accessing these. + +## Downgrading at Runtime + +If you are using multiple pieces of software built with mismatched versions of OTIO, you may need to configure the newer one(s) to write out OTIO in an older format without recompiling or modifying the software. + +You can accomplish this in two ways: +- The {term}`OTIO_DEFAULT_TARGET_VERSION_FAMILY_LABEL` environment variable can specify a family and version. +- The `otioconvert` utility program can downgrade an OTIO file to an older version. + +### OTIO_DEFAULT_TARGET_VERSION_FAMILY_LABEL Environment Variable + +If your software uses OTIO's Python adapter system, then you can set the {term}`OTIO_DEFAULT_TARGET_VERSION_FAMILY_LABEL` environment variable with a `FAMILY:VERSION` value. +For example, in a *nix shell: `env OTIO_DEFAULT_TARGET_VERSION_FAMILY_LABEL=OTIO_CORE:0.14.0 my_program` + +The `OTIO_CORE` family is pre-populated with the core OTIO schema versions for previous OTIO releases, for example `0.14.0`. If you have custom schema that needs to be downgraded as well, you will need to specify your own family and version mapping, as described above. + +### Downgrading with otioconvert + +If your software uses OTIO's C++ API, then it does not look for the {term}`OTIO_DEFAULT_TARGET_VERSION_FAMILY_LABEL` environment variable, but you can convert an OTIO file after it has been created with the `otioconvert` utility. + +You can either use a family like this: +``` +env OTIO_DEFAULT_TARGET_VERSION_FAMILY_LABEL=OTIO_CORE:0.14.0 otioconvert -i input.otio -o output.otio +``` + +or you can specify the version mapping for each schema you care about like this: +``` +otioconvert -i input.otio -o output.otio -A target_schema_versions="{'Clip':1, 'Timeline':1, 'Marker':2}" +``` + +## For Developers + +During the development of OpenTimelineIO schemas, whether they are in the core or in plugins, it is expected that schemas will change and evolve over time. Here are some processes for doing that. + +### Changing a Field + +Given `SimpleClass`: ```python import opentimelineio as otio @otio.core.register_type -class SimpleClass(otio.core.SerializeableObject): - serializeable_label = "SimpleClass.1" - my_field = otio.core.serializeable_field("my_field", int) +class SimpleClass(otio.core.SerializableObject): + serializable_label = "SimpleClass.1" + my_field = otio.core.serializable_field("my_field", int) ``` - -And you want to change `my_field` to `new_field`. To do this: +And `my_field` needs to be renamed to `new_field`. To do this: - Make the change in the class - Bump the version number in the label -- add an upgrade function - -So after the changes, you'll have: +- add upgrade and downgrade functions ```python @otio.core.register_type -class SimpleClass(otio.core.SerializeableObject): - serializeable_label = "SimpleClass.2" - my_field = otio.core.serializeable_field("new_field", int) +class SimpleClass(otio.core.SerializableObject): + serializable_label = "SimpleClass.2" + new_field = otio.core.serializable_field("new_field", int) @otio.core.upgrade_function_for(SimpleClass, 2) def upgrade_one_to_two(data): return {"new_field" : data["my_field"] } + +@otio.core.downgrade_function_from(SimpleClass, 2) +def downgrade_two_to_one(data): + return {"my_field": data["new_field"]} ``` -Lets change it again, so that `new_field` becomes `even_newer_field`. +Changing it again, now `new_field` becomes `even_newer_field`. ```python @otio.core.register_type -class SimpleClass(otio.core.SerializeableObject): - serializeable_label = "SimpleClass.2" - my_field = otio.core.serializeable_field("even_newer_field", int) +class SimpleClass(otio.core.SerializableObject): + serializable_label = "SimpleClass.2" + even_newer_field = otio.core.serializable_field("even_newer_field", int) @otio.core.upgrade_function_for(SimpleClass, 2) def upgrade_one_to_two(data): @@ -57,39 +294,51 @@ def upgrade_one_to_two(data): @otio.core.upgrade_function_for(SimpleClass, 3) def upgrade_two_to_three(data): return {"even_newer_field" : data["new_field"] } -``` -Upgrade functions can be sparse - if version `3` to `4` doesn't require a function, for example, you don't need to write one. +@otio.core.downgrade_function_from(SimpleClass, 2) +def downgrade_two_to_one(data): + return {"my_field": data["new_field"]} -Adding or Removing a Field --------------------------------- +# ...and corresponding second downgrade function +@otio.core.downgrade_function_from(SimpleClass, 3) +def downgrade_two_to_one(data): + return {"new_field": data["even_newer_field"]} +``` + +### Adding or Removing a Field Starting from the same class: ```python @otio.core.register_type -class SimpleClass(otio.core.SerializeableObject): - serializeable_label = "SimpleClass.1" - my_field = otio.core.serializeable_field("my_field", int) +class SimpleClass(otio.core.SerializableObject): + serializable_label = "SimpleClass.1" + my_field = otio.core.serializable_field("my_field", int) ``` -Adding or Removing a field is simpler. In these cases, you don't need to write an upgrade function, since any new classes will be initialized through the constructor, and any removed fields will be ignored when reading from an older schema version. +If a change to a schema is to add a field, for which the default value is the correct value for an old schema, then no upgrade or downgrade function is needed. The parser ignores values that aren't in the schema. + +Additionally, upgrade functions will be called in order, but they need not cover every version number. So if there is an upgrade function for version 2 and 4, to get to version 4, OTIO will automatically apply function 2 and then function 4 in order, skipping the missing 3. + +Downgrade functions must be called in order with no gaps. -So lets add a new field: +Example of adding a field (`other_field`): ```python @otio.core.register_type -class SimpleClass(otio.core.SerializeableObject): - serializeable_label = "SimpleClass.2" - my_field = otio.core.serializeable_field("my_field", int) - other_field = otio.core.serializeable_field("other_field", int) +class SimpleClass(otio.core.SerializableObject): + serializable_label = "SimpleClass.2" + my_field = otio.core.serializable_field("my_field", int) + other_field = otio.core.serializable_field("other_field", int) ``` -And then delete the original field: +Removing a field (`my_field`): ```python @otio.core.register_type -class SimpleClass(otio.core.SerializeableObject): - serializeable_label = "SimpleClass.3" - other_field = otio.core.serializeable_field("other_field", int) -``` \ No newline at end of file +class SimpleClass(otio.core.SerializableObject): + serializable_label = "SimpleClass.3" + other_field = otio.core.serializable_field("other_field", int) +``` + +Similarly, when deleting a field, if the field is now ignored and does not contribute to computation, no upgrade or downgrade function is needed. diff --git a/docs/tutorials/write-a-hookscript.md b/docs/tutorials/write-a-hookscript.md index 0942e0d8f..f9cd7d0a3 100644 --- a/docs/tutorials/write-a-hookscript.md +++ b/docs/tutorials/write-a-hookscript.md @@ -31,7 +31,6 @@ To create a new OTIO hook script, you need to create a file myhooks.py. Then add { "OTIO_SCHEMA" : "HookScript.1", "name" : "example hook", - "execution_scope" : "in process", "filepath" : "myhooks.py" } ], @@ -46,7 +45,8 @@ To create a new OTIO hook script, you need to create a file myhooks.py. Then add The ``hook_scripts`` section will register the plugin with the system, and the ``hooks`` section will attach the scripts to hooks. -Then you need to add this manifest to your `$OTIO_PLUGIN_MANIFEST_PATH` environment variable (which is separated with `:` for POSIX or `;` for Windows). You may also define media linkers and adapters via the same manifest. +Then you need to add this manifest to your {term}`OTIO_PLUGIN_MANIFEST_PATH` environment variable. +You may also define media linkers and adapters via the same manifest. ## Running a Hook Script diff --git a/docs/tutorials/write-a-media-linker.md b/docs/tutorials/write-a-media-linker.md index 086019c4c..3f922b2de 100644 --- a/docs/tutorials/write-a-media-linker.md +++ b/docs/tutorials/write-a-media-linker.md @@ -4,9 +4,11 @@ OpenTimelineIO Media Linkers are plugins that allow OTIO to replace MissingRefer The current MediaLinker can be specified as an argument to `otio.adapters.read_from_file` or via an environment variable. If one is specified, then it will run after the adapter reads the contents of the file before it is returned back. +```python #/usr/bin/env python import opentimelineio as otio mytimeline = otio.adapters.read_from_file("something.edl", media_linker_name="awesome_studios_media_linker") +``` After the EDL adapter reads something.edl, the media linker "awesome_studios_media_linker" will run and link the media in the file (if it can). @@ -16,27 +18,30 @@ After the EDL adapter reads something.edl, the media linker "awesome_studios_med To create a new OTIO Adapter, you need to create a file mymedialinker.py. Then add a manifest that points at that python file: +```json { "OTIO_SCHEMA" : "PluginManifest.1", "media_linkers" : [ { "OTIO_SCHEMA" : "MediaLinker.1", "name" : "awesome_studios_media_linker", - "execution_scope" : "in process", "filepath" : "mymedialinker.py" } ], "adapters" : [ ] } +``` -Then you need to add this manifest to your `$OTIO_PLUGIN_MANIFEST_PATH` environment variable (which is separated with `:` for POSIX or `;` for Windows). +Then you need to add this manifest to your {term}`OTIO_PLUGIN_MANIFEST_PATH` environment variable. -Finally, to specify this linker as the default media linker, set `OTIO_DEFAULT_MEDIA_LINKER` to the name of the media linker: +Finally, to specify this linker as the default media linker, set {term}`OTIO_DEFAULT_MEDIA_LINKER` to the name of the media linker: - setenv OTIO_DEFAULT_MEDIA_LINKER "awesome_studios_media_linker" +```bash + export OTIO_DEFAULT_MEDIA_LINKER="awesome_studios_media_linker" +``` -To package and share your media linker, follow [these instructions](write-an-adapter.html#packaging-and-sharing-custom-adapters). +To package and share your media linker, follow [these instructions](write-an-adapter.md#packaging-and-sharing-custom-adapters). ## Writing a Media Linker @@ -44,6 +49,7 @@ To write a media linker, write a python file with a "link_media_reference" funct For example: +```python def link_media_reference(in_clip, media_linker_argument_map): d.update(media_linker_argument_map) # you'll probably want to set it to something other than a missing reference @@ -51,10 +57,12 @@ For example: name=in_clip.name + "_tweaked", metadata=d ) +``` ## For Testing The otioconvert.py script has a --media-linker argument you can use to test out your media linker (once its on the path). +```bash env OTIO_PLUGIN_MANIFEST_PATH=mymanifest.otio.json:$OTIO_PLUGIN_MANIFEST_PATH bin/otioconvert.py -i somefile.edl --media-linker="awesome_studios_media_linker" -o /var/tmp/test.otio - +``` diff --git a/docs/tutorials/write-a-schemadef.md b/docs/tutorials/write-a-schemadef.md index fffd50745..2bcfaecd3 100644 --- a/docs/tutorials/write-a-schemadef.md +++ b/docs/tutorials/write-a-schemadef.md @@ -76,7 +76,6 @@ Then you must add it to a plugin manifest: { "OTIO_SCHEMA" : "SchemaDef.1", "name" : "mything", - "execution_scope" : "in process", "filepath" : "mything.py" } ] @@ -85,8 +84,8 @@ Then you must add it to a plugin manifest: The same plugin manifest may also include adapters and media linkers, if desired. -Then you need to add this manifest to your `$OTIO_PLUGIN_MANIFEST_PATH` environment -variable (which is separated with `:` for POSIX or `;` for Windows). +Then you need to add this manifest to your {term}`OTIO_PLUGIN_MANIFEST_PATH` environment +variable. ## Using the New Schema in Your Code diff --git a/docs/tutorials/write-an-adapter.md b/docs/tutorials/write-an-adapter.md index a08041636..3b52b73b4 100644 --- a/docs/tutorials/write-an-adapter.md +++ b/docs/tutorials/write-an-adapter.md @@ -15,30 +15,18 @@ The otio.adapters module will look at the file extension (in this case ".edl" or Note that the OTIO JSON format is treated like an adapter as well. The ".otio" format is the only format that is lossless. It can store and retrieve all of the objects, metadata and features available in OpenTimelineIO. Other formats are lossy - they will only store and retrieve features that are supported by that format (and by the adapter implementation). Some adapters may choose to put extra information, not supported by OTIO, into metadata on any OTIO object. -## Registering Your Contrib Adapter +## Sharing an Adapter You’ve Written With the Community -To create a new contrib OTIO Adapter, you need to create a file `myadapter.py` in the `opentimelineio_contrib/adapters` folder. Then add an entry to `opentimelineio_contrib/adapters/contrib_adapters.plugin_manifest.json` that registers your new adapter. Note that contrib adapters are community supported, and not supported by the OTIO team. They must still provide unit tests and pass both testing and linting before they are accepted into the repository. +If you've written an adapter that might be useful to others, please contact the [OpenTimelineIO team](https://github.com/AcademySoftwareFoundation/OpenTimelineIO) +so we can add it to the list of [Tools and Projects Using OpenTimelineIO](https://github.com/AcademySoftwareFoundation/OpenTimelineIO/wiki/Tools-and-Projects-Using-OpenTimelineIO). If the adapter is of broad enough interest to adopt as an OTIO community supported adapter, we can discuss transitioning it to the [OpenTimelineIO GitHub Organization](https://github.com/OpenTimelineIO/). Keep in mind, code should be [Apache 2.0](https://www.apache.org/licenses/LICENSE-2.0.txt) or [MIT](https://opensource.org/licenses/MIT) licensed if it is intended to transition to the OpenTimelineIO project. -### Custom Adapters - -Alternately, if you are creating a site specific adapter that you do _not_ intend to share with the community, you can create your `myadapter.py` file anywhere. In this case, you must create a `mysite.plugin_manifest.json` (with an entry like the below example that points at `myadapter.py`) and then put the path to your `mysite.plugin_manifest.json` on your `$OTIO_PLUGIN_MANIFEST_PATH` environment variable (which is separated with `:` for POSIX or `;` for Windows). - -For example, to register `myadapter.py` that supports files with a `.myext` file extension: -```json -{ - "OTIO_SCHEMA" : "Adapter.1", - "name" : "myadapter", - "execution_scope" : "in process", - "filepath" : "myadapter.py", - "suffixes" : ["myext"] -} -``` +### Packaging and Sharing Custom Adapters -Currently (as of OTIO Beta 8) only execution_scope "in process" is supported. If your adapter needs to execute non-Python code, then we intend to support execution_scope "external process" in the future. +Adapters may also be organized into their own independent Python project for subsequent [packaging](https://packaging.python.org/tutorials/packaging-projects/#generating-distribution-archives), [distribution](https://packaging.python.org/tutorials/packaging-projects/#uploading-the-distribution-archives) and [installation](https://packaging.python.org/tutorials/packaging-projects/#installing-your-newly-uploaded-package) by [`pip`](https://packaging.python.org/key_projects/#pip). -### Packaging and Sharing Custom Adapters +The easist way is to [otio-plugin-template repo](https://github.com/OpenTimelineIO/otio-plugin-template) and click "Use this template". This will get you started with plugin boilerplate and allow you to develop the adapter in your own GitHub account. -Adapters may also be organized into their own independent Python project for subsequent [packaging](https://packaging.python.org/tutorials/packaging-projects/#generating-distribution-archives), [distribution](https://packaging.python.org/tutorials/packaging-projects/#uploading-the-distribution-archives) and [installation](https://packaging.python.org/tutorials/packaging-projects/#installing-your-newly-uploaded-package) by [`pip`](https://packaging.python.org/key_projects/#pip). We recommend you organize your project like so: +If you'd like to work from scratch, we recommend you organize your project like so: ``` . ├── setup.py @@ -85,14 +73,12 @@ And a `plugin_manifest.json` like: { "OTIO_SCHEMA" : "Adapter.1", "name" : "adapter_x", - "execution_scope" : "in process", "filepath" : "adapters/my_adapter_x.py", "suffixes" : ["xxx"] }, { "OTIO_SCHEMA" : "Adapter.1", "name" : "adapter_y", - "execution_scope" : "in process", "filepath" : "adapters/my_adapter_y.py", "suffixes" : ["yyy", "why"] } @@ -101,13 +87,26 @@ And a `plugin_manifest.json` like: { "OTIO_SCHEMA" : "MediaLinker.1", "name" : "my_studios_media_linker", - "execution_scope" : "in process", "filepath" : "operations/my_linker.py" } ] } ``` +### Custom Adapters + +Alternately, if you are creating a site specific adapter that you do _not_ intend to share with the community, you can create your `myadapter.py` file anywhere. In this case, you must create a `mysite.plugin_manifest.json` (with an entry like the below example that points at `myadapter.py`) and then put the path to your `mysite.plugin_manifest.json` on your {term}`OTIO_PLUGIN_MANIFEST_PATH` environment variable. + +For example, to register `myadapter.py` that supports files with a `.myext` file extension: +```json +{ + "OTIO_SCHEMA" : "Adapter.1", + "name" : "myadapter", + "filepath" : "myadapter.py", + "suffixes" : ["myext"] +} +``` + ## Required Functions Each adapter must implement at least one of these functions: diff --git a/docs/use-cases/animation-shot-frame-ranges.md b/docs/use-cases/animation-shot-frame-ranges.md index a52a17163..ecaf71fd5 100644 --- a/docs/use-cases/animation-shot-frame-ranges.md +++ b/docs/use-cases/animation-shot-frame-ranges.md @@ -5,7 +5,7 @@ ## Summary This case is very similar to the -Shots Added/Removed from the Cut Use Case. +[](/use-cases/shots-added-removed-from-cut). The editorial and animation departments are working with a sequence of shots simultaneously over the course of a few weeks. The initial delivery of rendered video clips from animation to editorial provides enough footage for the editor(s) to work with, at least as a starting point. As the cut evolves, the editor(s) may need more frames at the @@ -26,7 +26,7 @@ runs a Python script which compares the frame range of each shot used in the cut take of each shot being animated. Any shot that is too short must be extended and any shot that is more than 12 frames too long can be trimmed down. The revised shots are animated, re-rendered and re-delivered to editorial. Upon receiving these new deliveries, editorial will cut them into the sequence (see also -Use Case: Conform New Renders into the Cut). +[](/use-cases/conform-new-renders-into-cut)). For shots that used timing effects to temporarily extend them, those effects can be removed, since the new version of those shots is now longer. @@ -35,8 +35,8 @@ those shots is now longer. - EDL reading - Clip names for video track - Source frame range for each clip - - Timing effects -- AAF reading + - [Timing effects](https://github.com/AcademySoftwareFoundation/OpenTimelineIO/issues/39) +- [AAF reading](https://github.com/AcademySoftwareFoundation/OpenTimelineIO/issues/1) - Clip names across all video tracks, subclips, etc. - Source frame range for each clip - Timing effects @@ -48,7 +48,7 @@ those shots is now longer. - Name - Metadata - Timing effects -- Timing effects +- [Timing effects](https://github.com/AcademySoftwareFoundation/OpenTimelineIO/issues/39) - Source frame range of each clip as effected by timing effects. - Composition - Clips in lower tracks that are obscured (totally or partially) by overlapping clips in higher tracks are considered trimmed or hidden. diff --git a/docs/use-cases/shots-added-removed-from-cut.md b/docs/use-cases/shots-added-removed-from-cut.md index a0b1d811a..34e55690e 100644 --- a/docs/use-cases/shots-added-removed-from-cut.md +++ b/docs/use-cases/shots-added-removed-from-cut.md @@ -36,7 +36,7 @@ deliver the new shot to editorial when it is ready. - EDL reading (done) - Clip names across all tracks -- AAF reading +- [AAF reading](https://github.com/AcademySoftwareFoundation/OpenTimelineIO/issues/1) - Clip names across all tracks, subclips, etc. - Timeline should include (done) - a Stack of tracks, each of which is a Sequence diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt index de02cade0..960a99625 100644 --- a/examples/CMakeLists.txt +++ b/examples/CMakeLists.txt @@ -8,6 +8,8 @@ include_directories(${PROJECT_SOURCE_DIR}/src list(APPEND examples conform) list(APPEND examples flatten_video_tracks) list(APPEND examples summarize_timing) +list(APPEND examples io_perf_test) +list(APPEND examples upgrade_downgrade_example) if(OTIO_PYTHON_INSTALL) list(APPEND examples python_adapters_child_process) list(APPEND examples python_adapters_embed) diff --git a/examples/build_simple_timeline.py b/examples/build_simple_timeline.py index 1fae5eeca..ac377f6aa 100755 --- a/examples/build_simple_timeline.py +++ b/examples/build_simple_timeline.py @@ -1,4 +1,7 @@ #!/usr/bin/env python +# +# SPDX-License-Identifier: Apache-2.0 +# Copyright Contributors to the OpenTimelineIO project __doc__ = """ Generate a simple timeline from scratch and write it to the specified path. @@ -72,7 +75,7 @@ def main(): # attach the reference to the clip cl = otio.schema.Clip( - name="Clip{}".format(i + 1), + name=f"Clip{i + 1}", media_reference=ref, # the source range represents the range of the media that is being diff --git a/examples/conform.cpp b/examples/conform.cpp index db43f5e99..842f5f3bb 100644 --- a/examples/conform.cpp +++ b/examples/conform.cpp @@ -1,26 +1,5 @@ -// +// SPDX-License-Identifier: Apache-2.0 // Copyright Contributors to the OpenTimelineIO project -// -// Licensed under the Apache License, Version 2.0 (the "Apache License") -// with the following modification; you may not use this file except in -// compliance with the Apache License and the following modification to it: -// Section 6. Trademarks. is deleted and replaced with: -// -// 6. Trademarks. This License does not grant permission to use the trade -// names, trademarks, service marks, or product names of the Licensor -// and its affiliates, except as required to comply with Section 4(c) of -// the License and to reproduce the content of the NOTICE file. -// -// You may obtain a copy of the Apache License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the Apache License with the above modification is -// distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -// KIND, either express or implied. See the Apache License for the specific -// language governing permissions and limitations under the Apache License. -// // Example OTIO script that reads a timeline and then relinks clips // to movie files found in a given folder, based on matching clip names to filenames. @@ -98,7 +77,7 @@ int conform_timeline( int count = 0; otio::ErrorStatus error_status; - const auto clips = timeline->clip_if(&error_status); + const auto clips = timeline->find_clips(&error_status); if (otio::is_error(error_status)) { examples::print_error(error_status); @@ -151,7 +130,7 @@ int main(int argc, char** argv) examples::print_error(error_status); exit(1); } - const auto clips = timeline->clip_if(&error_status); + const auto clips = timeline->find_clips(&error_status); if (otio::is_error(error_status)) { examples::print_error(error_status); diff --git a/examples/conform.py b/examples/conform.py index 1e4cff883..4d7bb7dba 100755 --- a/examples/conform.py +++ b/examples/conform.py @@ -1,27 +1,7 @@ #!/usr/bin/env python # +# SPDX-License-Identifier: Apache-2.0 # Copyright Contributors to the OpenTimelineIO project -# -# Licensed under the Apache License, Version 2.0 (the "Apache License") -# with the following modification; you may not use this file except in -# compliance with the Apache License and the following modification to it: -# Section 6. Trademarks. is deleted and replaced with: -# -# 6. Trademarks. This License does not grant permission to use the trade -# names, trademarks, service marks, or product names of the Licensor -# and its affiliates, except as required to comply with Section 4(c) of -# the License and to reproduce the content of the NOTICE file. -# -# You may obtain a copy of the Apache License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the Apache License with the above modification is -# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -# KIND, either express or implied. See the Apache License for the specific -# language governing permissions and limitations under the Apache License. -# """Example OTIO script that reads a timeline and then relinks clips to movie files found in a given folder, based on matching clip names to filenames. @@ -92,7 +72,7 @@ def _find_matching_media(name, folder): # shot = asset_database.find_shot(clip.metadata['mystudio']['shotID']) # new_media = shot.latest_render(format='mov') - matches = glob.glob("{0}/{1}.*".format(folder, name)) + matches = glob.glob(f"{folder}/{name}.*") matches = list(map(os.path.abspath, matches)) if not matches: @@ -102,7 +82,7 @@ def _find_matching_media(name, folder): return matches[0] else: print( - "WARNING: {0} matches found for clip '{1}', using '{2}'".format( + "WARNING: {} matches found for clip '{}', using '{}'".format( len(matches), name, matches[0] @@ -119,7 +99,7 @@ def _conform_timeline(timeline, folder): count = 0 - for clip in timeline.each_clip(): + for clip in timeline.find_clips(): # look for a media file that matches the clip's name new_path = _find_matching_media(clip.name, folder) @@ -143,12 +123,12 @@ def main(): timeline = otio.adapters.read_from_file(args.input) count = _conform_timeline(timeline, args.folder) - print("Relinked {0} clips to new media.".format(count)) + print(f"Relinked {count} clips to new media.") otio.adapters.write_to_file(timeline, args.output) print( "Saved {} with {} clips.".format( args.output, - len(list(timeline.each_clip())) + len(list(timeline.find_clips())) ) ) diff --git a/examples/flatten_video_tracks.cpp b/examples/flatten_video_tracks.cpp index 960e4ede8..db3c3c4a4 100644 --- a/examples/flatten_video_tracks.cpp +++ b/examples/flatten_video_tracks.cpp @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Contributors to the OpenTimelineIO project + #include "util.h" #include diff --git a/examples/flatten_video_tracks.py b/examples/flatten_video_tracks.py index b4cb37157..b27e8e3ed 100755 --- a/examples/flatten_video_tracks.py +++ b/examples/flatten_video_tracks.py @@ -1,27 +1,7 @@ #!/usr/bin/env python # +# SPDX-License-Identifier: Apache-2.0 # Copyright Contributors to the OpenTimelineIO project -# -# Licensed under the Apache License, Version 2.0 (the "Apache License") -# with the following modification; you may not use this file except in -# compliance with the Apache License and the following modification to it: -# Section 6. Trademarks. is deleted and replaced with: -# -# 6. Trademarks. This License does not grant permission to use the trade -# names, trademarks, service marks, or product names of the Licensor -# and its affiliates, except as required to comply with Section 4(c) of -# the License and to reproduce the content of the NOTICE file. -# -# You may obtain a copy of the Apache License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the Apache License with the above modification is -# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -# KIND, either express or implied. See the Apache License for the specific -# language governing permissions and limitations under the Apache License. -# import opentimelineio as otio import sys @@ -42,11 +22,11 @@ # Take just the video tracks - and flatten them into one. # This will trim away any overlapping segments, collapsing everything # into a single track. -print("Flattening {} video tracks into one...".format(len(video_tracks))) +print(f"Flattening {len(video_tracks)} video tracks into one...") onetrack = otio.algorithms.flatten_stack(video_tracks) # Now make a new empty Timeline and put that one Track into it -newtimeline = otio.schema.Timeline(name="{} Flattened".format(timeline.name)) +newtimeline = otio.schema.Timeline(name=f"{timeline.name} Flattened") newtimeline.tracks[:] = [onetrack] # keep the audio track(s) as-is diff --git a/examples/io_perf_test.cpp b/examples/io_perf_test.cpp new file mode 100644 index 000000000..920adfbeb --- /dev/null +++ b/examples/io_perf_test.cpp @@ -0,0 +1,263 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Contributors to the OpenTimelineIO project + +#include + +#include "opentimelineio/clip.h" +#include "opentimelineio/typeRegistry.h" +#include "opentimelineio/any.h" +#include "opentimelineio/serialization.h" +#include "opentimelineio/deserialization.h" +#include "opentimelineio/timeline.h" + +#include "util.h" + +namespace otio = opentimelineio::OPENTIMELINEIO_VERSION; + +using chrono_time_point = std::chrono::steady_clock::time_point; + +const struct { + bool FIXED_TMP = true; + bool PRINT_CPP_VERSION_FAMILY = false; + bool TO_JSON_STRING = true; + bool TO_JSON_STRING_NO_DOWNGRADE = true; + bool TO_JSON_FILE = true; + bool TO_JSON_FILE_NO_DOWNGRADE = true; + bool CLONE_TEST = true; + bool SINGLE_CLIP_DOWNGRADE_TEST = true; +} RUN_STRUCT ; + +// typedef std::chrono::duration fsec; + // auto t0 = Time::now(); + // auto t1 = Time::now(); + // fsec fs = t1 - t0; + +/// utility function for printing std::chrono elapsed time +double +print_elapsed_time( + const std::string& message, + const chrono_time_point& begin, + const chrono_time_point& end +) +{ + const std::chrono::duration dur = end - begin; + + std::cout << message << ": " << dur.count() << " [s]" << std::endl; + + return dur.count(); +} + +void +print_version_map() +{ + std::cerr << "current version map: " << std::endl; + for (const auto& kv_lbl: otio::CORE_VERSION_MAP) + { + std::cerr << " " << kv_lbl.first << std::endl; + for (auto kv_schema_version : kv_lbl.second) + { + std::cerr << " \"" << kv_schema_version.first << "\": "; + std::cerr << kv_schema_version.second << std::endl; + } + } + +} + +int +main( + int argc, + char *argv[] +) +{ + if (RUN_STRUCT.PRINT_CPP_VERSION_FAMILY) + { + print_version_map(); + } + + if (argc < 2) + { + std::cerr << "usage: otio_io_perf_test path/to/timeline.otio "; + std::cerr << "[--keep-tmp]" << std::endl; + return 1; + } + + bool keep_tmp = false; + if (argc > 2) + { + const std::string arg = argv[2]; + if (arg == "--keep-tmp") + { + keep_tmp = true; + } + } + + const std::string tmp_dir_path = ( + RUN_STRUCT.FIXED_TMP + ? "/var/tmp/ioperftest" + : examples::create_temp_dir() + ); + + otio::ErrorStatus err; + assert(!otio::is_error(err)); + + otio::schema_version_map downgrade_manifest = { + {"FakeSchema", 3}, + {"Clip", 1}, + {"OtherThing", 12000} + }; + + if (RUN_STRUCT.CLONE_TEST) + { + otio::SerializableObject::Retainer cl = new otio::Clip("test"); + cl->metadata()["example thing"] = "banana"; + const auto intermediate = cl->clone(&err); + assert(intermediate != nullptr); + const auto cl_clone = dynamic_cast(intermediate); + assert(cl_clone != nullptr); + assert(!otio::is_error(err)); + assert(cl->name() == cl_clone->name()); + } + + if (RUN_STRUCT.SINGLE_CLIP_DOWNGRADE_TEST) + { + otio::SerializableObject::Retainer cl = new otio::Clip("test"); + cl->metadata()["example thing"] = "banana"; + chrono_time_point begin = std::chrono::steady_clock::now(); + cl->to_json_file( + examples::normalize_path(tmp_dir_path + "/clip.otio"), + &err, + &downgrade_manifest + ); + chrono_time_point end = std::chrono::steady_clock::now(); + assert(!otio::is_error(err)); + print_elapsed_time("downgrade clip", begin, end); + } + + otio::any tl; + std::string fname = std::string(argv[1]); + + // read file + chrono_time_point begin = std::chrono::steady_clock::now(); + otio::SerializableObject::Retainer timeline( + dynamic_cast( + otio::Timeline::from_json_file( + examples::normalize_path(argv[1]), + &err + ) + ) + ); + chrono_time_point end = std::chrono::steady_clock::now(); + assert(!otio::is_error(err)); + if (!timeline) + { + examples::print_error(err); + return 1; + } + + print_elapsed_time("deserialize_json_from_file", begin, end); + + + double str_dg, str_nodg; + if (RUN_STRUCT.TO_JSON_STRING) + { + begin = std::chrono::steady_clock::now(); + const std::string result = timeline.value->to_json_string( + &err, + &downgrade_manifest + ); + end = std::chrono::steady_clock::now(); + assert(!otio::is_error(err)); + + if (otio::is_error(err)) + { + examples::print_error(err); + return 1; + } + str_dg = print_elapsed_time("serialize_json_to_string", begin, end); + } + + if (RUN_STRUCT.TO_JSON_STRING_NO_DOWNGRADE) + { + begin = std::chrono::steady_clock::now(); + const std::string result = timeline.value->to_json_string(&err, {}); + end = std::chrono::steady_clock::now(); + assert(!otio::is_error(err)); + + if (otio::is_error(err)) + { + examples::print_error(err); + return 1; + } + str_nodg = print_elapsed_time( + "serialize_json_to_string [no downgrade]", + begin, + end + ); + } + + if (RUN_STRUCT.TO_JSON_STRING && RUN_STRUCT.TO_JSON_STRING_NO_DOWNGRADE) + { + std::cout << " JSON to string no_dg/dg: " << str_dg / str_nodg; + std::cout << std::endl; + } + + double file_dg, file_nodg; + if (RUN_STRUCT.TO_JSON_FILE) + { + begin = std::chrono::steady_clock::now(); + timeline.value->to_json_file( + examples::normalize_path(tmp_dir_path + "/io_perf_test.otio"), + &err, + &downgrade_manifest + ); + end = std::chrono::steady_clock::now(); + assert(!otio::is_error(err)); + file_dg = print_elapsed_time("serialize_json_to_file", begin, end); + } + + if (RUN_STRUCT.TO_JSON_FILE_NO_DOWNGRADE) + { + begin = std::chrono::steady_clock::now(); + timeline.value->to_json_file( + examples::normalize_path( + tmp_dir_path + + "/io_perf_test.nodowngrade.otio" + ), + &err, + {} + ); + end = std::chrono::steady_clock::now(); + assert(!otio::is_error(err)); + file_nodg = print_elapsed_time( + "serialize_json_to_file [no downgrade]", + begin, + end + ); + } + + if (RUN_STRUCT.TO_JSON_FILE && RUN_STRUCT.TO_JSON_FILE_NO_DOWNGRADE) + { + std::cout << " JSON to file no_dg/dg: " << file_dg / file_nodg; + std::cout << std::endl; + } + + if (keep_tmp || RUN_STRUCT.FIXED_TMP) + { + std::cout << "Temp directory preserved. All files written to: "; + std::cout << tmp_dir_path << std::endl; + } + else + { + // clean up + const auto tmp_files = examples::glob(tmp_dir_path, "*"); + for (const auto& fp : tmp_files) + { + remove(fp.c_str()); + } + remove(tmp_dir_path.c_str()); + std::cout << "cleaned up tmp dir, pass --keep-tmp to preserve"; + std::cout << " output." << std::endl; + } + + return 0; +} diff --git a/examples/python_adapters_child_process.cpp b/examples/python_adapters_child_process.cpp index 9631bba4a..336961e4a 100644 --- a/examples/python_adapters_child_process.cpp +++ b/examples/python_adapters_child_process.cpp @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Contributors to the OpenTimelineIO project + // Example OTIO C++ code for reading and writing files supported by the OTIO // Python adapters. // diff --git a/examples/python_adapters_embed.cpp b/examples/python_adapters_embed.cpp index 4c0626410..5a6e67554 100644 --- a/examples/python_adapters_embed.cpp +++ b/examples/python_adapters_embed.cpp @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Contributors to the OpenTimelineIO project + // Example OTIO C++ code for reading and writing files supported by the OTIO // Python adapters. // diff --git a/examples/sample_plugin/otio_counter/__init__.py b/examples/sample_plugin/otio_counter/__init__.py index 0d0c0ac60..79645a9ec 100644 --- a/examples/sample_plugin/otio_counter/__init__.py +++ b/examples/sample_plugin/otio_counter/__init__.py @@ -1,4 +1,7 @@ -import pkg_resources +# SPDX-License-Identifier: Apache-2.0 +# Copyright Contributors to the OpenTimelineIO project + +import importlib.resources from opentimelineio.plugins import manifest @@ -25,7 +28,6 @@ { "OTIO_SCHEMA": "Adapter.1", "name": "track_counter", - "execution_scope": "in process", "filepath": "adapter.py", "suffixes": ["count"] } @@ -41,7 +43,6 @@ { "OTIO_SCHEMA" : "MediaLinker.1", "name" : "linker_example", - "execution_scope" : "in process", "filepath" : "linker.py" } @@ -62,6 +63,7 @@ def plugin_manifest(): # XXX: note, this doesn't get called. For an example of this working, # see the mockplugin unit test. + filepath = importlib.resources.files(__package__) / "plugin_manifest.json" return manifest.manifest_from_string( - pkg_resources.resource_string(__name__, 'plugin_manifest.json') + filepath.read_text() ) diff --git a/examples/sample_plugin/otio_counter/adapter.py b/examples/sample_plugin/otio_counter/adapter.py index 0608db011..504028d18 100644 --- a/examples/sample_plugin/otio_counter/adapter.py +++ b/examples/sample_plugin/otio_counter/adapter.py @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright Contributors to the OpenTimelineIO project import opentimelineio as otio """ @@ -10,7 +12,7 @@ def write_to_string(input_otio): - return '{}'.format(len(input_otio.tracks)) + return f'{len(input_otio.tracks)}' def read_from_string(input_str): diff --git a/examples/sample_plugin/otio_counter/plugin_manifest.json b/examples/sample_plugin/otio_counter/plugin_manifest.json index 79652914e..823e90dc0 100644 --- a/examples/sample_plugin/otio_counter/plugin_manifest.json +++ b/examples/sample_plugin/otio_counter/plugin_manifest.json @@ -4,7 +4,6 @@ { "OTIO_SCHEMA": "Adapter.1", "name": "track_counter", - "execution_scope": "in process", "filepath": "adapter.py", "suffixes": ["count"] } diff --git a/examples/sample_plugin/setup.py b/examples/sample_plugin/setup.py index 25ee2ed4c..d7f925738 100644 --- a/examples/sample_plugin/setup.py +++ b/examples/sample_plugin/setup.py @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright Contributors to the OpenTimelineIO project from setuptools import setup """ diff --git a/examples/shot_detect.py b/examples/shot_detect.py index 4c00fbd23..ef46e04c1 100755 --- a/examples/shot_detect.py +++ b/examples/shot_detect.py @@ -1,27 +1,7 @@ #!/usr/bin/env python # +# SPDX-License-Identifier: Apache-2.0 # Copyright Contributors to the OpenTimelineIO project -# -# Licensed under the Apache License, Version 2.0 (the "Apache License") -# with the following modification; you may not use this file except in -# compliance with the Apache License and the following modification to it: -# Section 6. Trademarks. is deleted and replaced with: -# -# 6. Trademarks. This License does not grant permission to use the trade -# names, trademarks, service marks, or product names of the Licensor -# and its affiliates, except as required to comply with Section 4(c) of -# the License and to reproduce the content of the NOTICE file. -# -# You may obtain a copy of the Apache License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the Apache License with the above modification is -# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -# KIND, either express or implied. See the Apache License for the specific -# language governing permissions and limitations under the Apache License. -# """Example OTIO script that generates an OTIO from a single quicktime by using ffprobe to detect shot breaks. @@ -102,7 +82,7 @@ def _media_start_end_of(media_path, fps): out, err = proc.communicate() if proc.returncode != 0: raise FFProbeFailedError( - "FFProbe Failed with error: {0}, output: {1}".format( + "FFProbe Failed with error: {}, output: {}".format( err, out ) ) @@ -178,7 +158,7 @@ def _timeline_with_breaks(name, full_path, dryrun=False): ).rescaled_to(fps) clip = otio.schema.Clip() - clip.name = "shot {0}".format(shot_index) + clip.name = f"shot {shot_index}" clip.source_range = otio.opentime.range_from_start_end_time( start_time, end_time_exclusive @@ -216,7 +196,7 @@ def _verify_ffprobe(): out, err = proc.communicate() if proc.returncode != 0: raise FFProbeFailedError( - "FFProbe Failed with error: {0}, output: {1}".format( + "FFProbe Failed with error: {}, output: {}".format( err, out ) ) @@ -229,14 +209,16 @@ def _ffprobe_fps(name, full_path, dryrun=False): name, full_path, dryrun, - arguments=["{0}".format(full_path)], + arguments=[f"{full_path}"], message="framerate" ) if dryrun: return 1.0 - for line in err.split('\n'): + err_str = err.decode("utf-8") + + for line in err_str.split('\n'): if not ("Stream" in line and "Video" in line): continue @@ -265,11 +247,11 @@ def _ffprobe_output( "compact=p=0", "-f", "lavfi", - "movie={0},select=gt(scene\\,.1)".format(full_path) + f"movie={full_path},select=gt(scene\\,.1)" ] if message: - print("Scanning {0} for {1}...".format(name, message)) + print(f"Scanning {name} for {message}...") cmd = ["ffprobe"] + arguments @@ -284,7 +266,7 @@ def _ffprobe_output( out, err = proc.communicate() if proc.returncode != 0: raise FFProbeFailedError( - "FFProbe Failed with error: {0}, output: {1}".format( + "FFProbe Failed with error: {}, output: {}".format( err, out ) ) @@ -313,7 +295,7 @@ def main(): otio_filename = os.path.splitext(name)[0] + ".otio" otio.adapters.write_to_file(new_tl, otio_filename) print( - "SAVED: {0} with {1} clips.".format( + "SAVED: {} with {} clips.".format( otio_filename, len(new_tl.tracks[0]) ) diff --git a/examples/summarize_timing.cpp b/examples/summarize_timing.cpp index d7ae3af89..9abf35271 100644 --- a/examples/summarize_timing.cpp +++ b/examples/summarize_timing.cpp @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Contributors to the OpenTimelineIO project + // Example OTIO C++ code that reads a timeline and then prints a summary // of the video clips found, including re-timing effects on each one. diff --git a/examples/summarize_timing.py b/examples/summarize_timing.py old mode 100644 new mode 100755 index d52eba3d7..d998ac656 --- a/examples/summarize_timing.py +++ b/examples/summarize_timing.py @@ -1,27 +1,7 @@ #!/usr/bin/env python # +# SPDX-License-Identifier: Apache-2.0 # Copyright Contributors to the OpenTimelineIO project -# -# Licensed under the Apache License, Version 2.0 (the "Apache License") -# with the following modification; you may not use this file except in -# compliance with the Apache License and the following modification to it: -# Section 6. Trademarks. is deleted and replaced with: -# -# 6. Trademarks. This License does not grant permission to use the trade -# names, trademarks, service marks, or product names of the Licensor -# and its affiliates, except as required to comply with Section 4(c) of -# the License and to reproduce the content of the NOTICE file. -# -# You may obtain a copy of the Apache License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the Apache License with the above modification is -# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -# KIND, either express or implied. See the Apache License for the specific -# language governing permissions and limitations under the Apache License. -# """Example OTIO script that reads a timeline and then prints a summary of the video clips found, including re-timing effects on each one. @@ -55,7 +35,7 @@ def _summarize_effects(item): def _summarize_range(label, time_range): if time_range is None: - print("\t{}: None".format(label)) + print(f"\t{label}: None") else: print( "\t{}: {} - {} (Duration: {})".format( @@ -74,8 +54,8 @@ def _summarize_range(label, time_range): def _summarize_timeline(timeline): # Here we iterate over each video track, and then just the top-level # items in each track. If you want to traverse the whole nested structure - # then you can use: for item in timeline.each_child() - # or just clips via: for clip in timeline.each_clip() + # then you can use: for item in timeline.find_children() + # or just clips via: for clip in timeline.find_clips() # See also: https://opentimelineio.readthedocs.io/en/latest/tutorials/otio-timeline-structure.html # noqa for track in timeline.video_tracks(): print( @@ -90,7 +70,7 @@ def _summarize_timeline(timeline): for item in track: if isinstance(item, otio.schema.Clip): clip = item - print("Clip: {}".format(clip.name)) + print(f"Clip: {clip.name}") # See the documentation to understand the difference # between each of these ranges: # https://opentimelineio.readthedocs.io/en/latest/tutorials/time-ranges.html diff --git a/examples/upgrade_downgrade_example.cpp b/examples/upgrade_downgrade_example.cpp new file mode 100644 index 000000000..532fc9abc --- /dev/null +++ b/examples/upgrade_downgrade_example.cpp @@ -0,0 +1,112 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Contributors to the OpenTimelineIO project + +#include "opentimelineio/serializableObject.h" +#include "opentimelineio/typeRegistry.h" +#include + +// demonstrates a minimal custom SerializableObject written in C++ with upgrade +// and downgrade functions + +namespace otio = opentimelineio::OPENTIMELINEIO_VERSION; + +// define the custom class +class SimpleClass : public otio::SerializableObject +{ +public: + struct Schema + { + static auto constexpr name = "SimpleClass"; + static int constexpr version = 2; + }; + + void set_new_field(int64_t val) { _new_field = val; } + int64_t new_field() const { return _new_field; } + +protected: + using Parent = SerializableObject; + + virtual ~SimpleClass() = default; + + // methods for serialization + virtual bool + read_from(Reader& reader) override + { + auto result = ( + reader.read("new_field", &_new_field) + && Parent::read_from(reader) + ); + + return result; + } + + // ...and deserialization + virtual void + write_to(Writer& writer) const override + { + Parent::write_to(writer); + writer.write("new_field", _new_field); + } + +private: + int64_t _new_field; +}; + +int +main( + int argc, + char *argv[] +) +{ + // register type and upgrade/downgrade functions + otio::TypeRegistry::instance().register_type(); + + // 1->2 + otio::TypeRegistry::instance().register_upgrade_function( + SimpleClass::Schema::name, + 2, + [](otio::AnyDictionary* d) + { + (*d)["new_field"] = (*d)["my_field"]; + d->erase("my_field"); + } + ); + // 2->1 + otio::TypeRegistry::instance().register_downgrade_function( + SimpleClass::Schema::name, + 2, + [](otio::AnyDictionary* d) + { + (*d)["my_field"] = (*d)["new_field"]; + d->erase("new_field"); + } + ); + + otio::ErrorStatus err; + + auto sc = otio::SerializableObject::Retainer(new SimpleClass()); + sc->set_new_field(12); + + // write it out to disk, without changing it + sc->to_json_file("/var/tmp/simpleclass.otio", &err); + + otio::schema_version_map downgrade_manifest = { + {"SimpleClass", 1} + }; + + // write it out to disk, downgrading to version 1 + sc->to_json_file("/var/tmp/simpleclass.otio", &err, &downgrade_manifest); + + // read it back, upgrading automatically back up to version 2 of the schema + otio::SerializableObject::Retainer sc2( + dynamic_cast( + SimpleClass::from_json_file("/var/tmp/simpleclass.otio", &err) + ) + ); + + assert(sc2->new_field() == sc->new_field()); + + std::cout << "Upgrade/Downgrade demo complete." << std::endl; + + return 0; +} diff --git a/examples/util.cpp b/examples/util.cpp index d252a731b..df8a43450 100644 --- a/examples/util.cpp +++ b/examples/util.cpp @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Contributors to the OpenTimelineIO project + #include "util.h" #include diff --git a/examples/util.h b/examples/util.h index bd27033da..671397658 100644 --- a/examples/util.h +++ b/examples/util.h @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Contributors to the OpenTimelineIO project + #pragma once #include diff --git a/maintainers/bump_version_number.py b/maintainers/bump_version_number.py new file mode 100644 index 000000000..c14ddcbb1 --- /dev/null +++ b/maintainers/bump_version_number.py @@ -0,0 +1,158 @@ +#!/usr/bin/env python +# +# SPDX-License-Identifier: Apache-2.0 +# Copyright Contributors to the OpenTimelineIO project + +__doc__ = """Manage and apply the version in the OTIO_VERSION.json file""" + +import argparse +import sys +import json + +OTIO_VERSION_JSON_PATH = "OTIO_VERSION.json" + + +def version(): + with open(OTIO_VERSION_JSON_PATH) as fi: + return json.load(fi)['version'] + + +def _parsed_args(): + parser = argparse.ArgumentParser( + description='Fetch a list of contributors for a given GitHub repo.' + ) + + op_grp = parser.add_mutually_exclusive_group(required=True) + op_grp.add_argument( + "-i", + "--increment", + type=str, + default=None, + choices=("major", "minor", "patch"), + help="Increment either the major or minor version number." + ) + op_grp.add_argument( + "-s", + "--set", + type=str, + default=None, + nargs=3, + help="Set the version string, in the form of MAJOR MINOR PATCH" + ) + op_grp.add_argument( + "-q", + "--query", + default=False, + action="store_true", + help="Query/print the current version without changing it" + ) + parser.add_argument( + "-d", + "--dryrun", + default=False, + action="store_true", + help="Perform actions but modify no files on disk." + ) + return parser.parse_args() + + +def main(): + args = _parsed_args() + + major, minor, patch = (int(v) for v in version()) + + if args.increment == "major": + major += 1 + minor = 0 + patch = 0 + elif args.increment == "minor": + minor += 1 + patch = 0 + elif args.increment == "patch": + patch += 1 + elif args.set: + major, minor, patch = args.set + elif args.query: + print(".".join(str(v) for v in (major, minor, patch))) + return + + print(f"Setting version to: {major}.{minor}.{patch}") + + # update the OTIO_VERSION file + with open(OTIO_VERSION_JSON_PATH, "w") as fo: + fo.write( + json.dumps({"version": [str(v) for v in (major, minor, patch)]}) + ) + print(f"Updated {OTIO_VERSION_JSON_PATH}") + + # update the CMakeLists.txt + with open("CMakeLists.txt") as fi: + cmake_input = fi.read() + + cmake_output = [] + key_map = {"MAJOR": major, "MINOR": minor, "PATCH": patch} + for ln in cmake_input.split("\n"): + for label, new_value in key_map.items(): + if f"set(OTIO_VERSION_{label} \"" in ln: + cmake_output.append( + f"set(OTIO_VERSION_{label} \"{new_value}\")" + ) + break + else: + cmake_output.append(ln) + + with open("CMakeLists.txt", 'w') as fo: + fo.write("\n".join(cmake_output)) + print("Updated {}".format("CMakeLists.txt")) + + # update the setup.py + with open("setup.py") as fi: + setup_input = fi.read() + + setup_output = [] + for ln in setup_input.split("\n"): + if "\"version\": " in ln: + + setup_output.append( + " \"version\": \"{}.{}.{}{}\",".format( + major, + minor, + patch, + (".dev1" in ln) and ".dev1" or "" + ) + ) + else: + setup_output.append(ln) + + with open("setup.py", 'w') as fo: + fo.write("\n".join(setup_output)) + print("Updated {}".format("setup.py")) + + +def add_suffix(content, version): + if version not in content: + sys.stderr.write( + "Version {} not found, suffix may have already been " + "added.\n".format(version) + ) + return False + + print("adding suffix, version will be: {}".format(version + ".dev1")) + content.replace(version, version + ".dev1") + return True + + +def remove_suffix(content, version): + if version + '.dev1' not in content: + sys.stderr.write( + "Version+Suffix {} not found, suffix may have already been " + "removed.\n".format(version + '.dev1') + ) + return False + + content.replace(version + ' .dev1', version) + return True + + +if __name__ == "__main__": + main() diff --git a/maintainers/download_gha_artifact.py b/maintainers/download_gha_artifact.py index 2abf51992..1525cd7ee 100644 --- a/maintainers/download_gha_artifact.py +++ b/maintainers/download_gha_artifact.py @@ -1,4 +1,8 @@ #!/usr/bin/env python3 +# +# SPDX-License-Identifier: Apache-2.0 +# Copyright Contributors to the OpenTimelineIO project + """ This script downloads an artifact from a GitHub Action workflow run, unzips and and stores the files in a directory of your choice. @@ -40,12 +44,12 @@ headers = { "Accept": "application/vnd.github.v3+json", - "Authorization": "token {args.token}".format(args=args), + "Authorization": f"token {args.token}", } if os.path.exists(args.directory) and os.listdir(args.directory): sys.stderr.write( - "{0!r} directory contains files. It should be empty.".format(args.directory) + f"{args.directory!r} directory contains files. It should be empty." ) sys.exit(1) @@ -53,7 +57,7 @@ os.makedirs(args.directory) request = urllib.request.Request( - "https://api.github.com/repos/PixarAnimationStudios/OpenTimelineIO/actions/runs?status=success", # noqa: E501 + "https://api.github.com/repos/AcademySoftwareFoundation/OpenTimelineIO/actions/runs?status=success", # noqa: E501 headers=headers, ) response = urllib.request.urlopen(request).read() @@ -64,7 +68,7 @@ break else: sys.stderr.write( - "No run for a workflow named {0!r} found for commit {1!r}.".format( + "No run for a workflow named {!r} found for commit {!r}.".format( args.workflow, args.sha ) ) @@ -72,14 +76,14 @@ print("Found workflow:") -print(" Name: {0}".format(workflow_run["name"])) -print(" Branch: {0}".format(workflow_run["head_branch"])) -print(" Commit: {0}".format(workflow_run["head_sha"])) -print(" Committer: {0}".format(workflow_run["head_commit"]["committer"])) -print(" Run Number: {0}".format(workflow_run["run_number"])) -print(" Status: {0}".format(workflow_run["status"])) -print(" Conclusion: {0}".format(workflow_run["conclusion"])) -print(" URL: {0}".format(workflow_run["html_url"])) +print(" Name: {}".format(workflow_run["name"])) +print(" Branch: {}".format(workflow_run["head_branch"])) +print(" Commit: {}".format(workflow_run["head_sha"])) +print(" Committer: {}".format(workflow_run["head_commit"]["committer"])) +print(" Run Number: {}".format(workflow_run["run_number"])) +print(" Status: {}".format(workflow_run["status"])) +print(" Conclusion: {}".format(workflow_run["conclusion"])) +print(" URL: {}".format(workflow_run["html_url"])) print("Getting list of artifacts") @@ -91,11 +95,11 @@ artifact_download_url = artifact["archive_download_url"] break else: - sys.stderr.write("No artifact named {0!r} found.".format(args.artifact)) + sys.stderr.write(f"No artifact named {args.artifact!r} found.") sys.exit(1) print( - "Downloading {0!r} artifact and unzipping to {1!r}".format( + "Downloading {!r} artifact and unzipping to {!r}".format( args.artifact, args.directory ) ) @@ -107,7 +111,7 @@ for zip_info in zip_file.infolist(): output_path = os.path.join(args.directory, zip_info.filename) - print("Writing {0!r} to {1!r}".format(zip_info.filename, output_path)) + print(f"Writing {zip_info.filename!r} to {output_path!r}") with open(output_path, "wb") as fd: fd.write(zip_file.open(zip_info).read()) diff --git a/maintainers/fetch_contributors.py b/maintainers/fetch_contributors.py index c5a112d85..974cf3107 100755 --- a/maintainers/fetch_contributors.py +++ b/maintainers/fetch_contributors.py @@ -1,8 +1,14 @@ #!/usr/bin/env python3 +# +# SPDX-License-Identifier: Apache-2.0 +# Copyright Contributors to the OpenTimelineIO project import argparse import json import urllib.request +import os + +CONTRIBUTORS_FILE = "CONTRIBUTORS.md" def parse_args(): @@ -12,11 +18,13 @@ def parse_args(): parser.add_argument( '--repo', required=True, - help='GitHub Project/Repo name. (e.g. "PixarAnimationStudios/OpenTimelineIO")' + help='GitHub Project/Repo name.' + ' (e.g. "AcademySoftwareFoundation/OpenTimelineIO")' ) parser.add_argument( '--token', - required=True, + required=False, + default=None, help='GitHub personal access token, used for authorization.' ' Get one here: https://github.com/settings/tokens/new' ) @@ -26,6 +34,13 @@ def parse_args(): def main(): args = parse_args() + token = args.token or os.environ.get("OTIO_RELEASE_GITHUB_TOKEN") + if not token: + raise RuntimeError( + "Error: a github token is required to run {}. Either pass it in " + "via --token or set $OTIO_RELEASE_GITHUB_TOKEN".format(__file__) + ) + # Note: un-authenticated requests have a strict rate limit. # We avoid this by using authentication for all our requests, # even the ones that don't need it. @@ -39,37 +54,81 @@ def main(): # response = urllib.request.urlopen(request).read().decode('utf-8') # print("Rate limit: {}".format(response)) + with open(CONTRIBUTORS_FILE) as fi: + input_contributors = fi.read() + request = urllib.request.Request( - "https://api.github.com/repos/{}/stats/contributors".format(args.repo), - headers={"Authorization": "token {}".format(args.token)} + f"https://api.github.com/repos/{args.repo}/stats/contributors", + headers={"Authorization": f"token {args.token}"} ) response = urllib.request.urlopen(request).read().decode('utf-8') - contributors = json.loads(response) + # this just ensures that response is really waited on so that json.loads + # works + print(f"Response size: {len(response)}") + + contributors = json.loads(response[:]) output_lines = [] if not contributors: print("No contributors found, something likely went wrong.") + print(response) for contributor in contributors: login = contributor['author']['login'] url = contributor['author']['html_url'] + total = contributor['total'] request = urllib.request.Request( - "https://api.github.com/users/{}".format(login), - headers={"Authorization": "token {}".format(args.token)} + f"https://api.github.com/users/{login}", + headers={"Authorization": f"token {args.token}"} ) response = urllib.request.urlopen(request).read().decode('utf-8') user = json.loads(response) name = user['name'] or "?" - # Print the output in markdown format - output_lines.append("* {} ([{}]({}))".format(name, login, url)) + if ( + login not in input_contributors + and name not in input_contributors + and "?" not in name + ): + print(f"Missing: {name} [{login}] # commits: {total}") + + # Print the output in markdown format + output_lines.append(f"* {name} ([{login}]({url}))") + + if output_lines: + # split the input_contributors into preamble and contributors list + split_contribs = input_contributors.split('\n') + + header = [] + body = [] + in_body = False + for ln in split_contribs: + if not in_body and ln.startswith("* "): + in_body = True + + if not in_body: + header.append(ln) + continue + + if ln.strip(): + body.append(ln) + + body.extend(output_lines) + body.sort(key=lambda v: v.lower()) + + result = '\n'.join(header + body) + + with open(CONTRIBUTORS_FILE, 'w') as fo: + fo.write(result) + else: + print(f"All contributors present in {CONTRIBUTORS_FILE}") - print("\n".join(sorted(output_lines, key=str.casefold))) + # print("\n".join(sorted(output_lines, key=str.casefold))) if __name__ == '__main__': diff --git a/maintainers/freeze_ci_versions.py b/maintainers/freeze_ci_versions.py new file mode 100644 index 000000000..aa230a097 --- /dev/null +++ b/maintainers/freeze_ci_versions.py @@ -0,0 +1,129 @@ +#!/usr/bin/env python +# +# SPDX-License-Identifier: Apache-2.0 +# Copyright Contributors to the OpenTimelineIO project + +__doc__ = """Freeze and unfreeze image versions for CI, part of the release +process. + +""" + +import argparse +import re +import urllib.request + +CI_WORKFLOW_FP = ".github/workflows/python-package.yml" +GITHUB_README_URL = ( + "https://raw.githubusercontent.com/actions/runner-images/main/README.md" +) +PLATFORMS = ["ubuntu", "macos", "windows"] + + +def _parsed_args(): + parser = argparse.ArgumentParser( + description='Fetch a list of contributors for a given GitHub repo.' + ) + + op_grp = parser.add_mutually_exclusive_group(required=True) + op_grp.add_argument( + "-f", + "--freeze", + default=False, + action="store_true", + help="freeze the ci version from latest to their version." + ) + op_grp.add_argument( + "-u", + "--unfreeze", + default=False, + action="store_true", + help="unfreeze the ci version from the version back to latest." + ) + parser.add_argument( + "-d", + "--dryrun", + default=False, + action="store_true", + help="Perform actions but modify no files on disk." + ) + return parser.parse_args() + + +def main(): + args = _parsed_args() + + request = urllib.request.Request(GITHUB_README_URL) + response = urllib.request.urlopen(request).read().decode('utf-8') + + # HACK: pull the image version corresponding to -latest out of the + # README.md for the github repo where they are stored + lines = response.split("\n") + plat_map = {} + for plat in PLATFORMS: + plat_latest = plat + "-latest" + for ln in lines: + if plat_latest not in ln: + continue + plat_map[plat] = ( + re.match(".*(" + plat + "-.*)`.*", ln).groups(0)[0] + ) + + if args.freeze: + freeze_ci(plat_map, args.dryrun) + + if args.unfreeze: + unfreeze_ci(plat_map, args.dryrun) + + +def freeze_ci(plat_map, dryrun=False): + modified = False + with open(CI_WORKFLOW_FP) as fi: + output_content = fi.read() + + for plat in plat_map: + plat_latest = plat + "-latest" + if plat_latest not in output_content: + print(f"Platform {plat} appears to already be frozen.") + continue + + output_content = output_content.replace(plat_latest, plat_map[plat]) + modified = True + print(f"Platform {plat} frozen to version: {plat_map[plat]}") + + if modified and not dryrun: + with open(CI_WORKFLOW_FP, 'w') as fo: + fo.write(output_content) + return True + + return False + + +def unfreeze_ci(plat_map, dryrun=False): + modified = False + with open(CI_WORKFLOW_FP) as fi: + output_content = fi.read() + + for plat, plat_current in plat_map.items(): + plat_latest = plat + "-latest" + if plat_current not in output_content: + print( + "Platform {} appears to already be set to -latest.".format( + plat + ) + ) + continue + + output_content = output_content.replace(plat_current, plat_latest) + modified = True + print(f"Platform {plat} unfrozen back to: {plat_latest}") + + if modified and not dryrun: + with open(CI_WORKFLOW_FP, 'w') as fo: + fo.write(output_content) + return True + + return False + + +if __name__ == "__main__": + main() diff --git a/maintainers/remove_dev_suffix.py b/maintainers/remove_dev_suffix.py new file mode 100644 index 000000000..ea7513bed --- /dev/null +++ b/maintainers/remove_dev_suffix.py @@ -0,0 +1,96 @@ +#!/usr/bin/env python +# +# SPDX-License-Identifier: Apache-2.0 +# Copyright Contributors to the OpenTimelineIO project + +__doc__ = """Strip or add the .dev1 suffix, part of the release process""" + +import argparse +import sys + +TARGET_FILES = [ + "setup.py" +] + + +def _parsed_args(): + parser = argparse.ArgumentParser( + description='Fetch a list of contributors for a given GitHub repo.' + ) + + op_grp = parser.add_mutually_exclusive_group(required=True) + op_grp.add_argument( + "-a", + "--add", + default=False, + action="store_true", + help="add the dev1 suffix to the version" + ) + op_grp.add_argument( + "-r", + "--remove", + default=False, + action="store_true", + help="remove the dev1 suffix to the version" + ) + parser.add_argument( + "-d", + "--dryrun", + default=False, + action="store_true", + help="Perform actions but modify no files on disk." + ) + return parser.parse_args() + + +def _target_version(): + import bump_version_number + return ".".join(bump_version_number.version()) + + +def main(): + args = _parsed_args() + + version = _target_version() + + for fp in TARGET_FILES: + with open(fp) as fi: + content = fi.read() + + if args.add: + modified = add_suffix(content, version) + elif args.remove: + modified = remove_suffix(content, version) + + if modified and not args.dryrun: + with open(fp, 'w') as fo: + fo.write(modified) + print(f"Wrote modified {fp}.") + + +def add_suffix(content, version): + if version not in content: + sys.stderr.write( + "Version {} not found, suffix may have already been " + "added.\n".format(version) + ) + return False + + print("adding suffix, version will be: {}".format(version + ".dev1")) + return content.replace(version, version + ".dev1") + + +def remove_suffix(content, version): + if version + '.dev1' not in content: + sys.stderr.write( + "Version+Suffix {} not found, suffix may have already been " + "removed.\n".format(version + '.dev1') + ) + return False + + print(f"removing suffix, version will be: {version}") + return content.replace(version + '.dev1', version) + + +if __name__ == "__main__": + main() diff --git a/maintainers/verify_license.py b/maintainers/verify_license.py new file mode 100755 index 000000000..d325ca9f4 --- /dev/null +++ b/maintainers/verify_license.py @@ -0,0 +1,138 @@ +#!/usr/bin/env python +# +# SPDX-License-Identifier: Apache-2.0 +# Copyright Contributors to the OpenTimelineIO project + +__doc__ = """The utility script checks to make sure that all of the source +files in the OpenTimelineIO project have the correct license header.""" + +import argparse +import os +import sys + +LICENSES = { + ".py": """# SPDX-License-Identifier: Apache-2.0 +# Copyright Contributors to the OpenTimelineIO project +""", + ".cpp": """// SPDX-License-Identifier: Apache-2.0 +// Copyright Contributors to the OpenTimelineIO project +""", + ".c": """// SPDX-License-Identifier: Apache-2.0 +// Copyright Contributors to the OpenTimelineIO project +""", + ".h": """// SPDX-License-Identifier: Apache-2.0 +// Copyright Contributors to the OpenTimelineIO project +""", + ".swift": """// SPDX-License-Identifier: Apache-2.0 +// Copyright Contributors to the OpenTimelineIO project +""", + ".mm": """// SPDX-License-Identifier: Apache-2.0 +// Copyright Contributors to the OpenTimelineIO project +""" +} + +# dependencies and build dir do not need to be checked +SKIP_DIRS = [ + os.path.join("src", "deps"), + "build", + ".git", +] + + +def _parsed_args(): + """ parse commandline arguments with argparse """ + + parser = argparse.ArgumentParser( + description=__doc__, + formatter_class=argparse.ArgumentDefaultsHelpFormatter + ) + parser.add_argument( + '-s', + '--start-dir', + default='.', + type=str, + help=("Directory to start searching for files in.") + ) + parser.add_argument( + '-f', + '--fix', + default=False, + action="store_true", + help="Fix licenses in place when possible" + ) + + return parser.parse_args() + + +def main(): + correct_license = 0 + incorrect_license = 0 + total = 0 + + args = _parsed_args() + + for root, dirs, files in os.walk(args.start_dir): + for filename in files: + # make sure the dependencies aren't checked + if any(d in root for d in SKIP_DIRS): + continue + fullpath = os.path.join(root, filename) + for ext, lic in LICENSES.items(): + if filename.endswith(ext): + total += 1 + try: + content = open(fullpath).read() + except Exception as ex: + sys.stderr.write( + "ERROR: Unable to read file: {}\n{}".format( + fullpath, + ex + ) + ) + continue + + if len(content) > 0 and lic not in content: + print(f"MISSING: {os.path.relpath(fullpath)}") + if args.fix: + content = LICENSES[os.path.splitext(fullpath)[1]] + with open(fullpath) as fi: + content += fi.read() + with open(fullpath, 'w') as fo: + fo.write(content) + print( + "...FIXED: {}".format( + os.path.relpath(fullpath) + ) + ) + incorrect_license += 1 + else: + correct_license += 1 + + print( + "{} of {} files have the correct license.".format( + correct_license, + total + ) + ) + + if incorrect_license != 0: + if not args.fix: + raise RuntimeError( + "ERROR: {} files do NOT have the correct license.\n".format( + incorrect_license + ) + ) + else: + print( + "{} files had the correct license added.".format( + incorrect_license + ) + ) + + +if __name__ == "__main__": + try: + main() + except RuntimeError as err: + sys.stderr.write(err.args[0]) + sys.exit(1) diff --git a/setup.py b/setup.py old mode 100755 new mode 100644 index 6c58a741d..c3cf6c48d --- a/setup.py +++ b/setup.py @@ -1,4 +1,7 @@ #! /usr/bin/env python +# +# SPDX-License-Identifier: Apache-2.0 +# Copyright Contributors to the OpenTimelineIO project """Setup.py for installing OpenTimelineIO @@ -9,10 +12,10 @@ import multiprocessing import os +import shlex import sys import platform import subprocess -import unittest import tempfile import shutil @@ -35,12 +38,11 @@ def _debugInstance(x): for a in sorted(dir(x)): - print("%s: %s" % (a, getattr(x, a))) + print("{}: {}".format(a, getattr(x, a))) -class CMakeExtension(Extension): - def __init__(self, name): - Extension.__init__(self, name, sources=[]) +def join_args(args): + return ' '.join(map(shlex.quote, args)) class OTIO_build_ext(setuptools.command.build_ext.build_ext): @@ -49,12 +51,24 @@ def initialize_options(self): super(setuptools.command.build_ext.build_ext, self).initialize_options() """ + built = False + def run(self): - # This works around the fact that we build _opentime and _otio - # extensions as a one-shot cmake invocation. Usually we'd build each - # separately using build_extension. self.announce('running OTIO build_ext', level=2) - self.build() + # Let the original build_ext class do its job. + # This is rather important because build_ext.run takes care of a + # couple of things, one of which is to copy the built files into + # the source tree (in src/py-opentimelineio/opentimelineio) + # when building in editable mode. + super().run() + + def build_extension(self, _ext: Extension): + # This works around the fact that we build _opentime and _otio + # extensions as a one-shot cmake invocation. Setuptools calls + # build_extension for each Extension registered in the setup function. + if not self.built: + self.build() + self.built = True def build(self): self.build_temp_dir = ( @@ -72,6 +86,12 @@ def build(self): self.cmake_generate() self.cmake_install() + def is_windows(self): + return platform.system() == "Windows" + + def is_mingw(self): + return self.plat_name.startswith('mingw') + def generate_cmake_arguments(self): # Use the provided build dir so setuptools will be able to locate and # either install to the correct location or package. @@ -96,9 +116,11 @@ def generate_cmake_arguments(self): # Python modules wil be installed by setuptools. '-DOTIO_INSTALL_PYTHON_MODULES:BOOL=OFF', ] - - if platform.system() == "Windows": - cmake_args += ["-A", PLAT_TO_CMAKE[self.plat_name]] + if self.is_windows(): + if self.is_mingw(): + cmake_args += ['-G Unix Makefiles'] + else: + cmake_args += ["-A", PLAT_TO_CMAKE[self.plat_name]] cxx_coverage = bool(os.environ.get("OTIO_CXX_COVERAGE_BUILD")) if cxx_coverage and not os.environ.get("OTIO_CXX_BUILD_TMP_DIR"): @@ -147,7 +169,10 @@ def cmake_preflight_check(self): def cmake_generate(self): self.announce('running cmake generation', level=2) + cmake_args = ['cmake', SOURCE_DIR] + self.generate_cmake_arguments() + self.announce(join_args(cmake_args), level=2) + subprocess.check_call( cmake_args, cwd=self.build_temp_dir, @@ -156,19 +181,22 @@ def cmake_generate(self): def cmake_install(self): self.announce('running cmake build', level=2) - if platform.system() == "Windows": + if self.is_windows() and not self.is_mingw(): multi_proc = '/m' else: - multi_proc = '-j{}'.format(multiprocessing.cpu_count()) + multi_proc = f'-j{multiprocessing.cpu_count()}' + + cmake_args = [ + 'cmake', + '--build', '.', + '--target', 'install', + '--config', self.build_config, + '--', multi_proc, + ] + self.announce(join_args(cmake_args), level=2) subprocess.check_call( - [ - 'cmake', - '--build', '.', - '--target', 'install', - '--config', self.build_config, - '--', multi_proc, - ], + cmake_args, cwd=self.build_temp_dir, env=os.environ.copy() ) @@ -176,11 +204,11 @@ def cmake_install(self): # check the python version first if ( - sys.version_info[0] < 2 or - (sys.version_info[0] == 2 and sys.version_info[1] < 7) + sys.version_info[0] < 3 or + (sys.version_info[0] == 3 and sys.version_info[1] < 7) ): sys.exit( - 'OpenTimelineIO requires python2.7 or greater, detected version:' + 'OpenTimelineIO requires python3.7 or greater, detected version:' ' {}.{}'.format( sys.version_info[0], sys.version_info[1] @@ -190,10 +218,10 @@ def cmake_install(self): # Metadata that gets stamped into the __init__ files during the build phase. PROJECT_METADATA = { - "version": "0.15.0.dev1", + "version": "0.16.0.dev1", "author": 'Contributors to the OpenTimelineIO project', "author_email": 'otio-discussion@lists.aswf.io', - "license": 'Modified Apache 2.0 License', + "license": 'Apache 2.0 License', } METADATA_TEMPLATE = """ @@ -221,7 +249,7 @@ def _append_version_info_to_init_scripts(build_lib): ) # get the base data from the original file - with open(source_file, 'r') as fi: + with open(source_file) as fi: src_data = fi.read() # write that + the suffix to the target file @@ -234,22 +262,17 @@ class OTIO_build_py(setuptools.command.build_py.build_py): """Stamps PROJECT_METADATA into __init__ files.""" def run(self): - setuptools.command.build_py.build_py.run(self) - - if not self.dry_run: + super().run() + + if not self.dry_run and not self.editable_mode: + # Only run when not in dry-mode (a dry run should not have any side effect) + # and in non-editable mode. We don't want to edit files when in editable + # mode because that could lead to modifications to the source files. + # Note that setuptools will set self.editable_mode to True + # when "pip install -e ." is run. _append_version_info_to_init_scripts(self.build_lib) -def test_otio(): - """Discovers and runs tests""" - try: - # Clear the environment of a preset media linker - del os.environ['OTIO_DEFAULT_MEDIA_LINKER'] - except KeyError: - pass - return unittest.TestLoader().discover('tests') - - # copied from first paragraph of README.md LONG_DESCRIPTION = """OpenTimelineIO is an interchange format and API for editorial cut information. OTIO is not a container format for media, rather it @@ -273,11 +296,11 @@ def test_otio(): url='http://opentimeline.io', project_urls={ 'Source': - 'https://github.com/PixarAnimationStudios/OpenTimelineIO', + 'https://github.com/AcademySoftwareFoundation/OpenTimelineIO', 'Documentation': 'https://opentimelineio.readthedocs.io/', 'Issues': - 'https://github.com/PixarAnimationStudios/OpenTimelineIO/issues', + 'https://github.com/AcademySoftwareFoundation/OpenTimelineIO/issues', }, classifiers=[ @@ -287,14 +310,13 @@ def test_otio(): 'Topic :: Multimedia :: Video :: Display', 'Topic :: Multimedia :: Video :: Non-Linear Editor', 'Topic :: Software Development :: Libraries :: Python Modules', - 'License :: Other/Proprietary License', - 'Programming Language :: Python :: 2', - 'Programming Language :: Python :: 2.7', + 'License :: OSI Approved :: Apache Software License', 'Programming Language :: Python :: 3', 'Programming Language :: Python :: 3.7', 'Programming Language :: Python :: 3.8', 'Programming Language :: Python :: 3.9', 'Programming Language :: Python :: 3.10', + 'Programming Language :: Python :: 3.11', 'Operating System :: OS Independent', 'Natural Language :: English', ], @@ -312,15 +334,20 @@ def test_otio(): ], }, - include_package_data=True, packages=( - find_packages(where="src/py-opentimelineio") + - find_packages(where="src") + - find_packages(where="contrib") + find_packages(where="src/py-opentimelineio") + # opentimelineio + find_packages(where="src") + # opentimelineview + find_packages(where="contrib", exclude=["opentimelineio_contrib.adapters.tests"]) # opentimelineio_contrib # noqa ), + ext_modules=[ - CMakeExtension('_opentimelineio'), - CMakeExtension('_opentime'), + # The full and correct module name is required here because + # setuptools needs to resolve the name to find the built file + # and copy it into the source tree. (Because yes, editable install + # means that the install should point to the source tree, which + # means the .sos need to also be there alongside the python files). + Extension('opentimelineio._otio', sources=[]), + Extension('opentimelineio._opentime', sources=[]), ], package_dir={ @@ -330,23 +357,20 @@ def test_otio(): }, # Disallow 3.9.0 because of https://github.com/python/cpython/pull/22670 - python_requires='>2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*, !=3.6.*, !=3.9.0', # noqa: E501 + python_requires='>=3.7, !=3.9.0', # noqa: E501 install_requires=[ - 'pyaaf2~=1.4.0', - 'backports.tempfile; python_version<"3.0"', - # Enables the builtins module in the XGES adapter - 'future; python_version<"3.0"', - # Used in the otioz adapter to conform to unix paths - 'pathlib2; python_version<"3.0"' + 'pyaaf2>=1.4,<1.7', + 'importlib_metadata>=1.4; python_version < "3.8"', ], entry_points={ 'console_scripts': [ - 'otioview = opentimelineview.console:main', 'otiocat = opentimelineio.console.otiocat:main', 'otioconvert = opentimelineio.console.otioconvert:main', - 'otiostat = opentimelineio.console.otiostat:main', 'otiopluginfo = opentimelineio.console.otiopluginfo:main', + 'otiostat = opentimelineio.console.otiostat:main', + 'otiotool = opentimelineio.console.otiotool:main', + 'otioview = opentimelineview.console:main', ( 'otioautogen_serialized_schema_docs = ' 'opentimelineio.console.autogen_serialized_datamodel:main' @@ -366,12 +390,6 @@ def test_otio(): ] }, - test_suite='setup.test_otio', - - tests_require=[ - 'mock;python_version<"3.3"', - ], - # because we need to open() the adapters manifest, we aren't zip-safe zip_safe=False, diff --git a/src/deps/Imath b/src/deps/Imath index bd254da1a..5b8627abc 160000 --- a/src/deps/Imath +++ b/src/deps/Imath @@ -1 +1 @@ -Subproject commit bd254da1a16fca608966ea3a725e9a5642b8ba05 +Subproject commit 5b8627abcf2829d88dbae7420689a370ec8d763f diff --git a/src/deps/any b/src/deps/any index f67bd5f8b..bfc77f2e4 160000 --- a/src/deps/any +++ b/src/deps/any @@ -1 +1 @@ -Subproject commit f67bd5f8bbf7eb628bf38206d4ac5cb22438e6bb +Subproject commit bfc77f2e4be6e9a093dd645ef3f1aa30620c9205 diff --git a/src/deps/optional-lite b/src/deps/optional-lite index a006f229a..be720ebfd 160000 --- a/src/deps/optional-lite +++ b/src/deps/optional-lite @@ -1 +1 @@ -Subproject commit a006f229a77b3b2dacf927e4029b8c1c60c86b52 +Subproject commit be720ebfd7add22abe60e49602735de9231105d0 diff --git a/src/deps/pybind11 b/src/deps/pybind11 index e7e2c79f3..4ce05175d 160000 --- a/src/deps/pybind11 +++ b/src/deps/pybind11 @@ -1 +1 @@ -Subproject commit e7e2c79f3f520f78ffc39fcb34f7919003102733 +Subproject commit 4ce05175d51a4685232452bdc1e9cbb13a240a65 diff --git a/src/deps/rapidjson b/src/deps/rapidjson index 8261c1ddf..06d58b9e8 160000 --- a/src/deps/rapidjson +++ b/src/deps/rapidjson @@ -1 +1 @@ -Subproject commit 8261c1ddf43f10de00fd8c9a67811d1486b2c784 +Subproject commit 06d58b9e848c650114556a23294d0b6440078c61 diff --git a/src/opentime/CMakeLists.txt b/src/opentime/CMakeLists.txt index 447853c4d..b7cc47cf4 100644 --- a/src/opentime/CMakeLists.txt +++ b/src/opentime/CMakeLists.txt @@ -42,14 +42,31 @@ if(OTIO_CXX_INSTALL) DESTINATION "${OTIO_RESOLVED_CXX_INSTALL_DIR}/include/opentime") install(TARGETS opentime - EXPORT OpenTimeConfig + EXPORT OpenTimeTargets INCLUDES DESTINATION "${OTIO_RESOLVED_CXX_INSTALL_DIR}/include" ARCHIVE DESTINATION "${OTIO_RESOLVED_CXX_DYLIB_INSTALL_DIR}" LIBRARY DESTINATION "${OTIO_RESOLVED_CXX_DYLIB_INSTALL_DIR}" RUNTIME DESTINATION "${OTIO_RESOLVED_CXX_DYLIB_INSTALL_DIR}") - install(EXPORT OpenTimeConfig + install(EXPORT OpenTimeTargets DESTINATION "${OTIO_RESOLVED_CXX_INSTALL_DIR}/share/opentime" NAMESPACE OTIO:: ) + + include(CMakePackageConfigHelpers) + configure_package_config_file( + ${CMAKE_CURRENT_SOURCE_DIR}/OpenTimeConfig.cmake.in + ${CMAKE_CURRENT_BINARY_DIR}/OpenTimeConfig.cmake + INSTALL_DESTINATION + ${OTIO_RESOLVED_CXX_INSTALL_DIR}/share/opentime + NO_SET_AND_CHECK_MACRO + NO_CHECK_REQUIRED_COMPONENTS_MACRO + ) + + install( + FILES + ${CMAKE_CURRENT_BINARY_DIR}/OpenTimeConfig.cmake + DESTINATION + ${OTIO_RESOLVED_CXX_INSTALL_DIR}/share/opentime + ) endif() diff --git a/src/opentime/OpenTimeConfig.cmake.in b/src/opentime/OpenTimeConfig.cmake.in new file mode 100644 index 000000000..421d55b32 --- /dev/null +++ b/src/opentime/OpenTimeConfig.cmake.in @@ -0,0 +1,3 @@ +@PACKAGE_INIT@ + +include("${CMAKE_CURRENT_LIST_DIR}/OpenTimeTargets.cmake") diff --git a/src/opentime/errorStatus.cpp b/src/opentime/errorStatus.cpp index e65a9d54a..13cd16db2 100644 --- a/src/opentime/errorStatus.cpp +++ b/src/opentime/errorStatus.cpp @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Contributors to the OpenTimelineIO project + #include "opentime/errorStatus.h" namespace opentime { namespace OPENTIME_VERSION { diff --git a/src/opentime/errorStatus.h b/src/opentime/errorStatus.h index 6855735de..1dc9ae0ef 100644 --- a/src/opentime/errorStatus.h +++ b/src/opentime/errorStatus.h @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Contributors to the OpenTimelineIO project + #pragma once #include "opentime/version.h" diff --git a/src/opentime/rationalTime.cpp b/src/opentime/rationalTime.cpp index 836c2b312..de7ead556 100644 --- a/src/opentime/rationalTime.cpp +++ b/src/opentime/rationalTime.cpp @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Contributors to the OpenTimelineIO project + #include "opentime/rationalTime.h" #include "opentime/stringPrintf.h" #include @@ -65,8 +68,8 @@ double RationalTime::nearest_valid_timecode_rate(double rate) { double nearest_rate = 0; - double min_diff = std::numeric_limits::max(); - for (auto valid_rate : smpte_timecode_rates) + double min_diff = std::numeric_limits::max(); + for (auto valid_rate: smpte_timecode_rates) { if (valid_rate == rate) { @@ -77,7 +80,7 @@ RationalTime::nearest_valid_timecode_rate(double rate) { continue; } - min_diff = diff; + min_diff = diff; nearest_rate = valid_rate; } return nearest_rate; @@ -91,9 +94,111 @@ is_dropframe_rate(double rate) return std::find(b, e, rate) != e; } +static bool +parseFloat( + char const* pCurr, + char const* pEnd, + bool allow_negative, + double* result) +{ + if (pCurr >= pEnd || !pCurr) + { + *result = 0.0; + return false; + } + + double ret = 0.0; + double sign = 1.0; + + if (*pCurr == '+') + { + ++pCurr; + } + else if (*pCurr == '-') + { + if (!allow_negative) + { + *result = 0.0; + return false; + } + sign = -1.0; + ++pCurr; + } + + // get integer part + // + // Note that uint64_t is used because overflow is well defined for + // unsigned integers, but it is undefined behavior for signed integers, + // and floating point values are couched in the specification with + // the caveat that an implementation may be IEEE-754 compliant, or only + // partially compliant. + // + uint64_t uintPart = 0; + while (pCurr < pEnd) + { + char c = *pCurr; + if (c < '0' || c > '9') + { + break; + } + uint64_t accumulated = uintPart * 10 + c - '0'; + if (accumulated < uintPart) + { + // if there are too many digits, resulting in an overflow, fail + *result = 0.0; + return false; + } + uintPart = accumulated; + ++pCurr; + } + + ret = static_cast(uintPart); + if (uintPart != static_cast(ret)) + { + // if the double cannot be casted precisely back to uint64_t, fail + // A double has 15 digits of precision, but a uint64_t can encode more. + *result = 0.0; + return false; + } + + // check for end of string or delimiter + if (pCurr == pEnd || *pCurr == '\0') + { + *result = sign * ret; + return true; + } + + // if the next character is not a decimal point, the string is malformed. + if (*pCurr != '.') + { + *result = 0.0; // zero consistent with earlier error condition + return false; + } + + ++pCurr; // skip decimal + + double position_scale = 0.1; + while (pCurr < pEnd) + { + char c = *pCurr; + if (c < '0' || c > '9') + { + break; + } + ret = ret + static_cast(c - '0') * position_scale; + ++pCurr; + position_scale *= 0.1; + } + + *result = sign * ret; + return true; +} + RationalTime RationalTime::from_timecode( - std::string const& timecode, double rate, ErrorStatus* error_status) + std::string const& timecode, + double rate, + ErrorStatus* error_status) { if (!RationalTime::is_valid_timecode_rate(rate)) { @@ -147,7 +252,7 @@ RationalTime::from_timecode( seconds = std::stoi(fields[2]); frames = std::stoi(fields[3]); } - catch (std::exception const& e) + catch (std::exception const&) { if (error_status) { @@ -195,67 +300,149 @@ RationalTime::from_timecode( // convert to frames const int value = - (((total_minutes * 60) + seconds) * nominal_fps + frames - - (dropframes * - (total_minutes - static_cast(std::floor(total_minutes / 10))))); + (((total_minutes * 60) + seconds) * nominal_fps + frames + - (dropframes + * (total_minutes + - static_cast(std::floor(total_minutes / 10))))); return RationalTime{ double(value), rate }; } +static void +set_error( + std::string const& time_string, + ErrorStatus::Outcome code, + ErrorStatus* err) +{ + if (err) + { + *err = ErrorStatus( + code, + string_printf( + "Error: '%s' - %s", + time_string.c_str(), + ErrorStatus::outcome_to_string(code).c_str())); + } +} + RationalTime RationalTime::from_time_string( - std::string const& time_string, double rate, ErrorStatus* error_status) + std::string const& time_string, + double rate, + ErrorStatus* error_status) { if (!RationalTime::is_valid_timecode_rate(rate)) { - if (error_status) - { - *error_status = ErrorStatus(ErrorStatus::INVALID_TIMECODE_RATE); - } + set_error( + time_string, + ErrorStatus::INVALID_TIMECODE_RATE, + error_status); return RationalTime::_invalid_time; } - std::vector fields(3, std::string()); - - // split the fields - int last_pos = 0; - - for (int i = 0; i < 2; i++) + const char* start = time_string.data(); + const char* end = start + time_string.length(); + char* current = const_cast(end); + char* parse_end = current; + char* prev_parse_end = current; + + double power[3] = { + 1.0, // seconds + 60.0, // minutes + 3600.0 // hours + }; + + double accumulator = 0.0; + int radix = 0; + while (start <= current) { - fields[i] = time_string.substr(last_pos, 2); - last_pos = last_pos + 3; - } - - fields[2] = time_string.substr(last_pos, time_string.length()); - - double hours, minutes, seconds; - - try - { - hours = std::stod(fields[0]); - minutes = std::stod(fields[1]); - seconds = std::stod(fields[2]); - } - catch (std::exception const& e) - { - if (error_status) + if (*current == ':') { - *error_status = ErrorStatus( + parse_end = current + 1; + char c = *parse_end; + if (c != '\0' && c != ':') + { + if (c < '0' || c > '9') + { + set_error( + time_string, + ErrorStatus::INVALID_TIME_STRING, + error_status); + return RationalTime::_invalid_time; + } + double val = 0.0; + if (!parseFloat(parse_end, prev_parse_end + 1, false, &val)) + { + set_error( + time_string, + ErrorStatus::INVALID_TIME_STRING, + error_status); + return RationalTime::_invalid_time; + } + prev_parse_end = nullptr; + if (radix < 2 && val >= 60.0) + { + set_error( + time_string, + ErrorStatus::INVALID_TIME_STRING, + error_status); + return RationalTime::_invalid_time; + } + accumulator += val * power[radix]; + } + ++radix; + if (radix == sizeof(power) / sizeof(power[0])) + { + set_error( + time_string, + ErrorStatus::INVALID_TIME_STRING, + error_status); + return RationalTime::_invalid_time; + } + } + else if ( + current < prev_parse_end && (*current < '0' || *current > '9') + && *current != '.') + { + set_error( + time_string, ErrorStatus::INVALID_TIME_STRING, - string_printf( - "Input time string '%s' is an invalid time string", - time_string.c_str())); + error_status); + return RationalTime::_invalid_time; + } + + if (start == current) + { + if (prev_parse_end) + { + double val = 0.0; + if (!parseFloat(start, prev_parse_end + 1, true, &val)) + { + set_error( + time_string, + ErrorStatus::INVALID_TIME_STRING, + error_status); + return RationalTime::_invalid_time; + } + accumulator += val * power[radix]; + } + break; + } + --current; + if (!prev_parse_end) + { + prev_parse_end = current; } - return RationalTime::_invalid_time; } - return from_seconds(seconds + minutes * 60 + hours * 60 * 60) - .rescaled_to(rate); + return from_seconds(accumulator).rescaled_to(rate); } std::string RationalTime::to_timecode( - double rate, IsDropFrameRate drop_frame, ErrorStatus* error_status) const + double rate, + IsDropFrameRate drop_frame, + ErrorStatus* error_status) const { if (error_status) { @@ -351,10 +538,11 @@ RationalTime::to_timecode( if (frames_over_ten_minutes > dropframes) { - value += (dropframes * 9 * ten_minute_chunks) + - dropframes * std::floor( - (frames_over_ten_minutes - dropframes) / - frames_per_minute); + value += (dropframes * 9 * ten_minute_chunks) + + dropframes + * std::floor( + (frames_over_ten_minutes - dropframes) + / frames_per_minute); } else { @@ -374,7 +562,12 @@ RationalTime::to_timecode( static_cast(std::floor(std::floor(seconds_total / 60) / 60)); return string_printf( - "%02d:%02d:%02d%c%02d", hours, minutes, seconds, div, frames); + "%02d:%02d:%02d%c%02d", + hours, + minutes, + seconds, + div, + frames); } std::string diff --git a/src/opentime/rationalTime.h b/src/opentime/rationalTime.h index ca6f8c4ed..fbdd02d4e 100644 --- a/src/opentime/rationalTime.h +++ b/src/opentime/rationalTime.h @@ -1,9 +1,13 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Contributors to the OpenTimelineIO project + #pragma once #include "opentime/errorStatus.h" #include "opentime/version.h" #include #include +#include #include namespace opentime { namespace OPENTIME_VERSION { @@ -35,7 +39,7 @@ class RationalTime , _rate{ rate } {} - constexpr RationalTime(RationalTime const&) noexcept = default; + constexpr RationalTime(RationalTime const&) noexcept = default; RationalTime& operator=(RationalTime const&) noexcept = default; bool is_invalid_time() const noexcept @@ -74,28 +78,30 @@ class RationalTime } static RationalTime constexpr duration_from_start_end_time( - RationalTime start_time, RationalTime end_time_exclusive) noexcept + RationalTime start_time, + RationalTime end_time_exclusive) noexcept { return start_time._rate == end_time_exclusive._rate - ? RationalTime{ end_time_exclusive._value - - start_time._value, + ? RationalTime{ end_time_exclusive._value + - start_time._value, start_time._rate } : RationalTime{ end_time_exclusive.value_rescaled_to( - start_time) - - start_time._value, + start_time) + - start_time._value, start_time._rate }; } static RationalTime constexpr duration_from_start_end_time_inclusive( - RationalTime start_time, RationalTime end_time_inclusive) noexcept + RationalTime start_time, + RationalTime end_time_inclusive) noexcept { return start_time._rate == end_time_inclusive._rate - ? RationalTime{ end_time_inclusive._value - - start_time._value + 1, + ? RationalTime{ end_time_inclusive._value - start_time._value + + 1, start_time._rate } : RationalTime{ end_time_inclusive.value_rescaled_to( - start_time) - - start_time._value + 1, + start_time) + - start_time._value + 1, start_time._rate }; } @@ -124,6 +130,11 @@ class RationalTime std::string const& timecode, double rate, ErrorStatus* error_status = nullptr); + + // parse a string in the form + // hours:minutes:seconds + // which may have a leading negative sign. seconds may have up to + // microsecond precision. static RationalTime from_time_string( std::string const& time_string, double rate, @@ -151,9 +162,13 @@ class RationalTime return to_timecode(_rate, IsDropFrameRate::InferFromRate, error_status); } + // produce a string in the form + // hours:minutes:seconds + // which may have a leading negative sign. seconds may have up to + // microsecond precision. std::string to_time_string() const; - RationalTime const& operator+=(RationalTime other) noexcept + RationalTime const& operator+=(RationalTime other) noexcept { if (_rate < other._rate) { @@ -167,7 +182,7 @@ class RationalTime return *this; } - RationalTime const& operator-=(RationalTime other) noexcept + RationalTime const& operator-=(RationalTime other) noexcept { if (_rate < other._rate) { @@ -185,11 +200,11 @@ class RationalTime operator+(RationalTime lhs, RationalTime rhs) noexcept { return (lhs._rate < rhs._rate) - ? RationalTime{ lhs.value_rescaled_to(rhs._rate) + - rhs._value, + ? RationalTime{ lhs.value_rescaled_to(rhs._rate) + + rhs._value, rhs._rate } - : RationalTime{ rhs.value_rescaled_to(lhs._rate) + - lhs._value, + : RationalTime{ rhs.value_rescaled_to(lhs._rate) + + lhs._value, lhs._rate }; } @@ -197,11 +212,11 @@ class RationalTime operator-(RationalTime lhs, RationalTime rhs) noexcept { return (lhs._rate < rhs._rate) - ? RationalTime{ lhs.value_rescaled_to(rhs._rate) - - rhs._value, + ? RationalTime{ lhs.value_rescaled_to(rhs._rate) + - rhs._value, rhs._rate } - : RationalTime{ lhs._value - - rhs.value_rescaled_to(lhs._rate), + : RationalTime{ lhs._value + - rhs.value_rescaled_to(lhs._rate), lhs._rate }; } diff --git a/src/opentime/stringPrintf.h b/src/opentime/stringPrintf.h index fb470880d..f05434655 100644 --- a/src/opentime/stringPrintf.h +++ b/src/opentime/stringPrintf.h @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Contributors to the OpenTimelineIO project + #pragma once #include "opentime/version.h" diff --git a/src/opentime/timeRange.cpp b/src/opentime/timeRange.cpp index e1634c04f..c57dd643e 100644 --- a/src/opentime/timeRange.cpp +++ b/src/opentime/timeRange.cpp @@ -1 +1,4 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Contributors to the OpenTimelineIO project + #include "opentime/timeRange.h" diff --git a/src/opentime/timeRange.h b/src/opentime/timeRange.h index 7a41dba9c..3c039c6da 100644 --- a/src/opentime/timeRange.h +++ b/src/opentime/timeRange.h @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Contributors to the OpenTimelineIO project + #pragma once #include "opentime/rationalTime.h" @@ -38,7 +41,8 @@ class TimeRange {} explicit constexpr TimeRange( - RationalTime start_time, RationalTime duration) noexcept + RationalTime start_time, + RationalTime duration) noexcept : _start_time{ start_time } , _duration{ duration } {} @@ -79,14 +83,17 @@ class TimeRange constexpr TimeRange extended_by(TimeRange other) const noexcept { - const RationalTime new_start_time{ std::min( - _start_time, other._start_time) }, - new_end_time{ std::max( - end_time_exclusive(), other.end_time_exclusive()) }; + const RationalTime new_start_time{ + std::min(_start_time, other._start_time) + }, + new_end_time{ + std::max(end_time_exclusive(), other.end_time_exclusive()) + }; return TimeRange{ new_start_time, RationalTime::duration_from_start_end_time( - new_start_time, new_end_time) }; + new_start_time, + new_end_time) }; } RationalTime clamped(RationalTime other) const noexcept @@ -98,8 +105,9 @@ class TimeRange { const TimeRange r{ std::max(other._start_time, _start_time), other._duration }; - const RationalTime end{ std::min( - r.end_time_exclusive(), end_time_exclusive()) }; + const RationalTime end{ + std::min(r.end_time_exclusive(), end_time_exclusive()) + }; return TimeRange{ r._start_time, end - r._start_time }; } @@ -139,14 +147,15 @@ class TimeRange * @param other */ constexpr bool contains( - TimeRange other, double epsilon_s = DEFAULT_EPSILON_s) const noexcept + TimeRange other, + double epsilon_s = DEFAULT_EPSILON_s) const noexcept { const double thisStart = _start_time.to_seconds(); const double thisEnd = end_time_exclusive().to_seconds(); const double otherStart = other._start_time.to_seconds(); const double otherEnd = other.end_time_exclusive().to_seconds(); - return greater_than(otherStart, thisStart, epsilon_s) && - lesser_than(otherEnd, thisEnd, epsilon_s); + return greater_than(otherStart, thisStart, epsilon_s) + && lesser_than(otherEnd, thisEnd, epsilon_s); } /** @@ -172,15 +181,16 @@ class TimeRange * @param epsilon_s */ constexpr bool overlaps( - TimeRange other, double epsilon_s = DEFAULT_EPSILON_s) const noexcept + TimeRange other, + double epsilon_s = DEFAULT_EPSILON_s) const noexcept { const double thisStart = _start_time.to_seconds(); const double thisEnd = end_time_exclusive().to_seconds(); const double otherStart = other._start_time.to_seconds(); const double otherEnd = other.end_time_exclusive().to_seconds(); - return lesser_than(thisStart, otherStart, epsilon_s) && - greater_than(thisEnd, otherStart, epsilon_s) && - greater_than(otherEnd, thisEnd, epsilon_s); + return lesser_than(thisStart, otherStart, epsilon_s) + && greater_than(thisEnd, otherStart, epsilon_s) + && greater_than(otherEnd, thisEnd, epsilon_s); } /** @@ -207,7 +217,8 @@ class TimeRange * @param epsilon_s */ constexpr bool before( - RationalTime other, double epsilon_s = DEFAULT_EPSILON_s) const noexcept + RationalTime other, + double epsilon_s = DEFAULT_EPSILON_s) const noexcept { const double thisEnd = end_time_exclusive().to_seconds(); const double otherTime = other.to_seconds(); @@ -246,8 +257,8 @@ class TimeRange const double thisEnd = end_time_exclusive().to_seconds(); const double otherStart = other._start_time.to_seconds(); const double otherEnd = other.end_time_exclusive().to_seconds(); - return fabs(otherStart - thisStart) <= epsilon_s && - lesser_than(thisEnd, otherEnd, epsilon_s); + return fabs(otherStart - thisStart) <= epsilon_s + && lesser_than(thisEnd, otherEnd, epsilon_s); } /** @@ -259,7 +270,8 @@ class TimeRange * @param other */ constexpr bool begins( - RationalTime other, double epsilon_s = DEFAULT_EPSILON_s) const noexcept + RationalTime other, + double epsilon_s = DEFAULT_EPSILON_s) const noexcept { const double thisStart = _start_time.to_seconds(); const double otherStart = other.to_seconds(); @@ -276,14 +288,15 @@ class TimeRange * @param epsilon_s */ constexpr bool finishes( - TimeRange other, double epsilon_s = DEFAULT_EPSILON_s) const noexcept + TimeRange other, + double epsilon_s = DEFAULT_EPSILON_s) const noexcept { const double thisStart = _start_time.to_seconds(); const double thisEnd = end_time_exclusive().to_seconds(); const double otherStart = other._start_time.to_seconds(); const double otherEnd = other.end_time_exclusive().to_seconds(); - return fabs(thisEnd - otherEnd) <= epsilon_s && - greater_than(thisStart, otherStart, epsilon_s); + return fabs(thisEnd - otherEnd) <= epsilon_s + && greater_than(thisStart, otherStart, epsilon_s); } /** @@ -296,7 +309,8 @@ class TimeRange * @param epsilon_s */ constexpr bool finishes( - RationalTime other, double epsilon_s = DEFAULT_EPSILON_s) const noexcept + RationalTime other, + double epsilon_s = DEFAULT_EPSILON_s) const noexcept { const double thisEnd = end_time_exclusive().to_seconds(); const double otherEnd = other.to_seconds(); @@ -313,14 +327,15 @@ class TimeRange * @param epsilon_s */ constexpr bool intersects( - TimeRange other, double epsilon_s = DEFAULT_EPSILON_s) const noexcept + TimeRange other, + double epsilon_s = DEFAULT_EPSILON_s) const noexcept { const double thisStart = _start_time.to_seconds(); const double thisEnd = end_time_exclusive().to_seconds(); const double otherStart = other._start_time.to_seconds(); const double otherEnd = other.end_time_exclusive().to_seconds(); - return lesser_than(thisStart, otherEnd, epsilon_s) && - greater_than(thisEnd, otherStart, epsilon_s); + return lesser_than(thisStart, otherEnd, epsilon_s) + && greater_than(thisEnd, otherStart, epsilon_s); } /** @@ -335,8 +350,8 @@ class TimeRange { const RationalTime start = lhs._start_time - rhs._start_time; const RationalTime duration = lhs._duration - rhs._duration; - return fabs(start.to_seconds()) < DEFAULT_EPSILON_s && - fabs(duration.to_seconds()) < DEFAULT_EPSILON_s; + return fabs(start.to_seconds()) < DEFAULT_EPSILON_s + && fabs(duration.to_seconds()) < DEFAULT_EPSILON_s; } /** @@ -350,19 +365,23 @@ class TimeRange } static constexpr TimeRange range_from_start_end_time( - RationalTime start_time, RationalTime end_time_exclusive) noexcept + RationalTime start_time, + RationalTime end_time_exclusive) noexcept { return TimeRange{ start_time, RationalTime::duration_from_start_end_time( - start_time, end_time_exclusive) }; + start_time, + end_time_exclusive) }; } static constexpr TimeRange range_from_start_end_time_inclusive( - RationalTime start_time, RationalTime end_time_inclusive) noexcept + RationalTime start_time, + RationalTime end_time_inclusive) noexcept { return TimeRange{ start_time, RationalTime::duration_from_start_end_time_inclusive( - start_time, end_time_inclusive) }; + start_time, + end_time_inclusive) }; } private: diff --git a/src/opentime/timeTransform.cpp b/src/opentime/timeTransform.cpp index 05bd73cd3..97201bfcc 100644 --- a/src/opentime/timeTransform.cpp +++ b/src/opentime/timeTransform.cpp @@ -1 +1,4 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Contributors to the OpenTimelineIO project + #include "opentime/timeTransform.h" diff --git a/src/opentime/timeTransform.h b/src/opentime/timeTransform.h index 6f90f9fde..435899114 100644 --- a/src/opentime/timeTransform.h +++ b/src/opentime/timeTransform.h @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Contributors to the OpenTimelineIO project + #pragma once #include "opentime/rationalTime.h" @@ -25,7 +28,7 @@ class TimeTransform constexpr double rate() const noexcept { return _rate; } - constexpr TimeTransform(TimeTransform const&) noexcept = default; + constexpr TimeTransform(TimeTransform const&) noexcept = default; TimeTransform& operator=(TimeTransform const&) noexcept = default; TimeRange applied_to(TimeRange other) const noexcept @@ -44,18 +47,17 @@ class TimeTransform RationalTime applied_to(RationalTime other) const noexcept { - RationalTime result{ - RationalTime{ other._value * _scale, other._rate } + _offset - }; - double target_rate = _rate > 0 ? _rate : other._rate; + RationalTime result{ RationalTime{ other._value * _scale, other._rate } + + _offset }; + double target_rate = _rate > 0 ? _rate : other._rate; return target_rate > 0 ? result.rescaled_to(target_rate) : result; } friend constexpr bool operator==(TimeTransform lhs, TimeTransform rhs) noexcept { - return lhs._offset == rhs._offset && lhs._scale == rhs._scale && - lhs._rate == rhs._rate; + return lhs._offset == rhs._offset && lhs._scale == rhs._scale + && lhs._rate == rhs._rate; } friend constexpr bool diff --git a/src/opentime/version.h b/src/opentime/version.h index 5fe0c03b8..58a822a04 100644 --- a/src/opentime/version.h +++ b/src/opentime/version.h @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Contributors to the OpenTimelineIO project + #pragma once #define OPENTIME_VERSION v1_0 diff --git a/src/opentimelineio/CMakeLists.txt b/src/opentimelineio/CMakeLists.txt index a5b484b47..1860b8e97 100644 --- a/src/opentimelineio/CMakeLists.txt +++ b/src/opentimelineio/CMakeLists.txt @@ -72,6 +72,7 @@ add_library(opentimelineio ${OTIO_SHARED_OR_STATIC_LIB} transition.cpp typeRegistry.cpp unknownSchema.cpp + CORE_VERSION_MAP.cpp ${OPENTIMELINEIO_HEADER_FILES}) add_library(OTIO::opentimelineio ALIAS opentimelineio) @@ -117,13 +118,30 @@ if(OTIO_CXX_INSTALL) endif() install(TARGETS opentimelineio - EXPORT OpenTimelineIOConfig + EXPORT OpenTimelineIOTargets INCLUDES DESTINATION "${OPENTIMELINEIO_INCLUDES}" ARCHIVE DESTINATION "${OTIO_RESOLVED_CXX_DYLIB_INSTALL_DIR}" LIBRARY DESTINATION "${OTIO_RESOLVED_CXX_DYLIB_INSTALL_DIR}" RUNTIME DESTINATION "${OTIO_RESOLVED_CXX_DYLIB_INSTALL_DIR}") - install(EXPORT OpenTimelineIOConfig + install(EXPORT OpenTimelineIOTargets DESTINATION "${OTIO_RESOLVED_CXX_INSTALL_DIR}/share/opentimelineio" NAMESPACE OTIO:: ) + + include(CMakePackageConfigHelpers) + configure_package_config_file( + ${CMAKE_CURRENT_SOURCE_DIR}/OpenTimelineIOConfig.cmake.in + ${CMAKE_CURRENT_BINARY_DIR}/OpenTimelineIOConfig.cmake + INSTALL_DESTINATION + ${OTIO_RESOLVED_CXX_INSTALL_DIR}/share/opentimelineio + NO_SET_AND_CHECK_MACRO + NO_CHECK_REQUIRED_COMPONENTS_MACRO + ) + + install( + FILES + ${CMAKE_CURRENT_BINARY_DIR}/OpenTimelineIOConfig.cmake + DESTINATION + ${OTIO_RESOLVED_CXX_INSTALL_DIR}/share/opentimelineio + ) endif() diff --git a/src/opentimelineio/CORE_VERSION_MAP.cpp b/src/opentimelineio/CORE_VERSION_MAP.cpp new file mode 100644 index 000000000..7a8c21aec --- /dev/null +++ b/src/opentimelineio/CORE_VERSION_MAP.cpp @@ -0,0 +1,116 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Contributors to the OpenTimelineIO project +// +// This document is automatically generated by running the `make version-map` +// make target. It is part of the unit tests suite and should be updated +// whenever schema versions change. If it needs to be updated, run: `make +// version-map-update` and this file should be regenerated. +// +// This maps a "Label" to a map of Schema name to Schema version. The intent is +// that these sets of schemas can be used for compatability with future +// versions of OTIO, so that a newer version of OTIO can target a compatability +// version of an older library. +#include "opentimelineio/typeRegistry.h" + +namespace opentimelineio { namespace OPENTIMELINEIO_VERSION { + +const label_to_schema_version_map CORE_VERSION_MAP{ + { "0.14.0", + { + { "Adapter", 1 }, + { "Clip", 1 }, + { "Composable", 1 }, + { "Composition", 1 }, + { "Effect", 1 }, + { "ExternalReference", 1 }, + { "FreezeFrame", 1 }, + { "Gap", 1 }, + { "GeneratorReference", 1 }, + { "HookScript", 1 }, + { "ImageSequenceReference", 1 }, + { "Item", 1 }, + { "LinearTimeWarp", 1 }, + { "Marker", 2 }, + { "MediaLinker", 1 }, + { "MediaReference", 1 }, + { "MissingReference", 1 }, + { "PluginManifest", 1 }, + { "SchemaDef", 1 }, + { "SerializableCollection", 1 }, + { "SerializableObject", 1 }, + { "SerializableObjectWithMetadata", 1 }, + { "Stack", 1 }, + { "TimeEffect", 1 }, + { "Timeline", 1 }, + { "Track", 1 }, + { "Transition", 1 }, + { "UnknownSchema", 1 }, + } }, + { "0.15.0", + { + { "Adapter", 1 }, + { "Clip", 2 }, + { "Composable", 1 }, + { "Composition", 1 }, + { "Effect", 1 }, + { "ExternalReference", 1 }, + { "FreezeFrame", 1 }, + { "Gap", 1 }, + { "GeneratorReference", 1 }, + { "HookScript", 1 }, + { "ImageSequenceReference", 1 }, + { "Item", 1 }, + { "LinearTimeWarp", 1 }, + { "Marker", 2 }, + { "MediaLinker", 1 }, + { "MediaReference", 1 }, + { "MissingReference", 1 }, + { "PluginManifest", 1 }, + { "SchemaDef", 1 }, + { "SerializableCollection", 1 }, + { "SerializableObject", 1 }, + { "SerializableObjectWithMetadata", 1 }, + { "Stack", 1 }, + { "Test", 1 }, + { "TimeEffect", 1 }, + { "Timeline", 1 }, + { "Track", 1 }, + { "Transition", 1 }, + { "UnknownSchema", 1 }, + } }, + { "0.16.0.dev1", + { + { "Adapter", 1 }, + { "Clip", 2 }, + { "Composable", 1 }, + { "Composition", 1 }, + { "Effect", 1 }, + { "ExternalReference", 1 }, + { "FreezeFrame", 1 }, + { "Gap", 1 }, + { "GeneratorReference", 1 }, + { "HookScript", 1 }, + { "ImageSequenceReference", 1 }, + { "Item", 1 }, + { "LinearTimeWarp", 1 }, + { "Marker", 2 }, + { "MediaLinker", 1 }, + { "MediaReference", 1 }, + { "MissingReference", 1 }, + { "PluginManifest", 1 }, + { "SchemaDef", 1 }, + { "SerializableCollection", 1 }, + { "SerializableObject", 1 }, + { "SerializableObjectWithMetadata", 1 }, + { "Stack", 1 }, + { "Test", 1 }, + { "TimeEffect", 1 }, + { "Timeline", 1 }, + { "Track", 1 }, + { "Transition", 1 }, + { "UnknownSchema", 1 }, + } }, + // {next} +}; + +}} // namespace opentimelineio::OPENTIMELINEIO_VERSION diff --git a/src/opentimelineio/CORE_VERSION_MAP.last.cpp b/src/opentimelineio/CORE_VERSION_MAP.last.cpp new file mode 100644 index 000000000..1eee987c6 --- /dev/null +++ b/src/opentimelineio/CORE_VERSION_MAP.last.cpp @@ -0,0 +1,84 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Contributors to the OpenTimelineIO project +// +// This document is automatically generated by running the `make version-map` +// make target. It is part of the unit tests suite and should be updated +// whenever schema versions change. If it needs to be updated, run: `make +// version-map-update` and this file should be regenerated. +// +// This maps a "Label" to a map of Schema name to Schema version. The intent is +// that these sets of schemas can be used for compatability with future +// versions of OTIO, so that a newer version of OTIO can target a compatability +// version of an older library. +#include "opentimelineio/typeRegistry.h" + +namespace opentimelineio { namespace OPENTIMELINEIO_VERSION { + +const label_to_schema_version_map CORE_VERSION_MAP{ + { "0.14.0", + { + { "Adapter", 1 }, + { "Clip", 1 }, + { "Composable", 1 }, + { "Composition", 1 }, + { "Effect", 1 }, + { "ExternalReference", 1 }, + { "FreezeFrame", 1 }, + { "Gap", 1 }, + { "GeneratorReference", 1 }, + { "HookScript", 1 }, + { "ImageSequenceReference", 1 }, + { "Item", 1 }, + { "LinearTimeWarp", 1 }, + { "Marker", 2 }, + { "MediaLinker", 1 }, + { "MediaReference", 1 }, + { "MissingReference", 1 }, + { "PluginManifest", 1 }, + { "SchemaDef", 1 }, + { "SerializableCollection", 1 }, + { "SerializableObject", 1 }, + { "SerializableObjectWithMetadata", 1 }, + { "Stack", 1 }, + { "TimeEffect", 1 }, + { "Timeline", 1 }, + { "Track", 1 }, + { "Transition", 1 }, + { "UnknownSchema", 1 }, + } }, + { "0.15.0", + { + { "Adapter", 1 }, + { "Clip", 2 }, + { "Composable", 1 }, + { "Composition", 1 }, + { "Effect", 1 }, + { "ExternalReference", 1 }, + { "FreezeFrame", 1 }, + { "Gap", 1 }, + { "GeneratorReference", 1 }, + { "HookScript", 1 }, + { "ImageSequenceReference", 1 }, + { "Item", 1 }, + { "LinearTimeWarp", 1 }, + { "Marker", 2 }, + { "MediaLinker", 1 }, + { "MediaReference", 1 }, + { "MissingReference", 1 }, + { "PluginManifest", 1 }, + { "SchemaDef", 1 }, + { "SerializableCollection", 1 }, + { "SerializableObject", 1 }, + { "SerializableObjectWithMetadata", 1 }, + { "Stack", 1 }, + { "Test", 1 }, + { "TimeEffect", 1 }, + { "Timeline", 1 }, + { "Track", 1 }, + { "Transition", 1 }, + { "UnknownSchema", 1 }, + } }, + // {next} +}; + +}} // namespace opentimelineio::OPENTIMELINEIO_VERSION diff --git a/src/opentimelineio/OpenTimelineIOConfig.cmake.in b/src/opentimelineio/OpenTimelineIOConfig.cmake.in new file mode 100644 index 000000000..f735b3666 --- /dev/null +++ b/src/opentimelineio/OpenTimelineIOConfig.cmake.in @@ -0,0 +1,6 @@ +@PACKAGE_INIT@ + +include(CMakeFindDependencyMacro) +find_dependency(OpenTime) + +include("${CMAKE_CURRENT_LIST_DIR}/OpenTimelineIOTargets.cmake") diff --git a/src/opentimelineio/any.h b/src/opentimelineio/any.h index 2939bae11..5a55ee448 100644 --- a/src/opentimelineio/any.h +++ b/src/opentimelineio/any.h @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Contributors to the OpenTimelineIO project + #pragma once #include "any/any.hpp" diff --git a/src/opentimelineio/anyDictionary.h b/src/opentimelineio/anyDictionary.h index 5a34e7d99..af5f5a29f 100644 --- a/src/opentimelineio/anyDictionary.h +++ b/src/opentimelineio/anyDictionary.h @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Contributors to the OpenTimelineIO project + #pragma once #include "opentimelineio/any.h" @@ -119,6 +122,65 @@ class AnyDictionary : private std::map map::swap(other); } + /// @TODO: remove all of these @{ + + // if key is in this, and the type of key matches the type of result, then + // set result to the value of any_cast(this[key]) and return true, + // otherwise return false + template + bool get_if_set(const std::string& key, containedType* result) const + { + if (result == nullptr) + { + return false; + } + + const auto it = this->find(key); + + if ((it != this->end()) + && (it->second.type().hash_code() + == typeid(containedType).hash_code())) + { + *result = any_cast(it->second); + return true; + } + else + { + return false; + } + } + + inline bool has_key(const std::string& key) const + { + return (this->find(key) != this->end()); + } + + // if key is in this, place the value in result and return true, otherwise + // store the value in result at key and return false + template + bool set_default(const std::string& key, containedType* result) + { + if (result == nullptr) + { + return false; + } + + const auto d_it = this->find(key); + + if ((d_it != this->end()) + && (d_it->second.type().hash_code() + == typeid(containedType).hash_code())) + { + *result = any_cast(d_it->second); + return true; + } + else + { + this->insert({ key, *result }); + return false; + } + } + using map::empty; using map::max_size; using map::size; @@ -158,7 +220,7 @@ class AnyDictionary : private std::map assert(d); } - MutationStamp(MutationStamp const&) = delete; + MutationStamp(MutationStamp const&) = delete; MutationStamp& operator=(MutationStamp const&) = delete; ~MutationStamp() diff --git a/src/opentimelineio/anyVector.h b/src/opentimelineio/anyVector.h index 606f36e52..38561fead 100644 --- a/src/opentimelineio/anyVector.h +++ b/src/opentimelineio/anyVector.h @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Contributors to the OpenTimelineIO project + #pragma once #include "opentimelineio/any.h" @@ -120,7 +123,7 @@ class AnyVector : private std::vector assert(v != nullptr); } - MutationStamp(MutationStamp const&) = delete; + MutationStamp(MutationStamp const&) = delete; MutationStamp& operator=(MutationStamp const&) = delete; ~MutationStamp() diff --git a/src/opentimelineio/clip.cpp b/src/opentimelineio/clip.cpp index e65ab6a2f..f390692c7 100644 --- a/src/opentimelineio/clip.cpp +++ b/src/opentimelineio/clip.cpp @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Contributors to the OpenTimelineIO project + #include "opentimelineio/clip.h" #include "opentimelineio/missingReference.h" @@ -56,8 +59,8 @@ Clip::check_for_valid_media_reference_key( { *error_status = ErrorStatus( ErrorStatus::MEDIA_REFERENCES_CONTAIN_EMPTY_KEY, - caller + - " failed because the media references contain an empty string key", + caller + + " failed because the media references contain an empty string key", this); } return false; @@ -70,8 +73,8 @@ Clip::check_for_valid_media_reference_key( { *error_status = ErrorStatus( ErrorStatus::MEDIA_REFERENCES_DO_NOT_CONTAIN_ACTIVE_KEY, - caller + - " failed because the media references do not contain the active key", + caller + + " failed because the media references do not contain the active key", this); } return false; @@ -111,7 +114,8 @@ Clip::active_media_reference_key() const noexcept void Clip::set_active_media_reference_key( - std::string const& new_active_key, ErrorStatus* error_status) noexcept + std::string const& new_active_key, + ErrorStatus* error_status) noexcept { if (!check_for_valid_media_reference_key( "set_active_media_reference_key", @@ -134,10 +138,11 @@ Clip::set_media_reference(MediaReference* media_reference) bool Clip::read_from(Reader& reader) { - return reader.read("media_references", &_media_references) && - reader.read( - "active_media_reference_key", &_active_media_reference_key) && - Parent::read_from(reader); + return reader.read("media_references", &_media_references) + && reader.read( + "active_media_reference_key", + &_active_media_reference_key) + && Parent::read_from(reader); } void @@ -179,7 +184,7 @@ Clip::available_range(ErrorStatus* error_status) const return active_media->available_range().value(); } -optional +optional Clip::available_image_bounds(ErrorStatus* error_status) const { auto active_media = media_reference(); @@ -189,7 +194,7 @@ Clip::available_image_bounds(ErrorStatus* error_status) const ErrorStatus::CANNOT_COMPUTE_BOUNDS, "No image bounds set on clip", this); - return optional(); + return optional(); } if (!active_media->available_image_bounds()) @@ -198,7 +203,7 @@ Clip::available_image_bounds(ErrorStatus* error_status) const ErrorStatus::CANNOT_COMPUTE_BOUNDS, "No image bounds set on media reference on clip", this); - return optional(); + return optional(); } return active_media->available_image_bounds(); diff --git a/src/opentimelineio/clip.h b/src/opentimelineio/clip.h index f1b34b6d4..0eea7131c 100644 --- a/src/opentimelineio/clip.h +++ b/src/opentimelineio/clip.h @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Contributors to the OpenTimelineIO project + #pragma once #include "opentimelineio/item.h" @@ -42,17 +45,17 @@ class Clip : public Item std::string const& new_active_key, ErrorStatus* error_status = nullptr) noexcept; - virtual TimeRange - available_range(ErrorStatus* error_status = nullptr) const; + TimeRange + available_range(ErrorStatus* error_status = nullptr) const override; - virtual optional - available_image_bounds(ErrorStatus* error_status) const; + optional + available_image_bounds(ErrorStatus* error_status) const override; protected: virtual ~Clip(); - virtual bool read_from(Reader&); - virtual void write_to(Writer&) const; + bool read_from(Reader&) override; + void write_to(Writer&) const override; private: template diff --git a/src/opentimelineio/composable.cpp b/src/opentimelineio/composable.cpp index a518c7c16..88e1299d4 100644 --- a/src/opentimelineio/composable.cpp +++ b/src/opentimelineio/composable.cpp @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Contributors to the OpenTimelineIO project + #include "opentimelineio/composable.h" #include "opentimelineio/composition.h" @@ -63,16 +66,19 @@ Composable::duration(ErrorStatus* error_status) const { if (error_status) { - *error_status = ErrorStatus::NOT_IMPLEMENTED; + *error_status = ErrorStatus( + ErrorStatus::OBJECT_WITHOUT_DURATION, + "Cannot determine duration from this kind of object", + this); } return RationalTime(); } -optional +optional Composable::available_image_bounds(ErrorStatus* error_status) const { *error_status = ErrorStatus::NOT_IMPLEMENTED; - return optional(); + return optional(); } }} // namespace opentimelineio::OPENTIMELINEIO_VERSION diff --git a/src/opentimelineio/composable.h b/src/opentimelineio/composable.h index d4d82039a..bd1d31df9 100644 --- a/src/opentimelineio/composable.h +++ b/src/opentimelineio/composable.h @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Contributors to the OpenTimelineIO project + #pragma once #include "opentimelineio/serializableObjectWithMetadata.h" @@ -31,7 +34,7 @@ class Composable : public SerializableObjectWithMetadata virtual RationalTime duration(ErrorStatus* error_status = nullptr) const; - virtual optional + virtual optional available_image_bounds(ErrorStatus* error_status) const; protected: @@ -45,8 +48,8 @@ class Composable : public SerializableObjectWithMetadata virtual ~Composable(); - virtual bool read_from(Reader&); - virtual void write_to(Writer&) const; + bool read_from(Reader&) override; + void write_to(Writer&) const override; private: Composition* _parent; diff --git a/src/opentimelineio/composition.cpp b/src/opentimelineio/composition.cpp index 96287f9d2..a9920f5c3 100644 --- a/src/opentimelineio/composition.cpp +++ b/src/opentimelineio/composition.cpp @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Contributors to the OpenTimelineIO project + #include "opentimelineio/composition.h" #include "opentimelineio/clip.h" #include "opentimelineio/vectorIndexing.h" @@ -42,7 +45,8 @@ Composition::clear_children() bool Composition::set_children( - std::vector const& children, ErrorStatus* error_status) + std::vector const& children, + ErrorStatus* error_status) { for (auto child: children) { @@ -68,7 +72,9 @@ Composition::set_children( bool Composition::insert_child( - int index, Composable* child, ErrorStatus* error_status) + int index, + Composable* child, + ErrorStatus* error_status) { if (child->parent()) { @@ -204,14 +210,15 @@ Composition::is_parent_of(Composable const* other) const std::pair, optional> Composition::handles_of_child( - Composable const* /* child */, ErrorStatus* /* error_status */) const + Composable const* /* child */, + ErrorStatus* /* error_status */) const { return std::make_pair(optional(), optional()); } int -Composition::_index_of_child( - Composable const* child, ErrorStatus* error_status) const +Composition::_index_of_child(Composable const* child, ErrorStatus* error_status) + const { for (size_t i = 0; i < _children.size(); i++) { @@ -231,7 +238,8 @@ Composition::_index_of_child( std::vector Composition::_path_from_child( - Composable const* child, ErrorStatus* error_status) const + Composable const* child, + ErrorStatus* error_status) const { auto current = child->parent(); std::vector parents{ current }; @@ -255,8 +263,8 @@ Composition::_path_from_child( } TimeRange -Composition::range_of_child_at_index( - int /* index */, ErrorStatus* error_status) const +Composition::range_of_child_at_index(int /* index */, ErrorStatus* error_status) + const { if (error_status) { @@ -267,7 +275,8 @@ Composition::range_of_child_at_index( TimeRange Composition::trimmed_range_of_child_at_index( - int /* index */, ErrorStatus* error_status) const + int /* index */, + ErrorStatus* error_status) const { if (error_status) { @@ -288,8 +297,8 @@ Composition::range_of_all_children(ErrorStatus* error_status) const // XXX should have reference_space argument or something TimeRange -Composition::range_of_child( - Composable const* child, ErrorStatus* error_status) const +Composition::range_of_child(Composable const* child, ErrorStatus* error_status) + const { auto parents = _path_from_child(child, error_status); if (is_error(error_status)) @@ -330,16 +339,18 @@ Composition::range_of_child( current = parent; } - return (reference_space != this) - ? transformed_time_range( - *result_range, reference_space, error_status) - : *result_range; + return (reference_space != this) ? transformed_time_range( + *result_range, + reference_space, + error_status) + : *result_range; } // XXX should have reference_space argument or something optional Composition::trimmed_range_of_child( - Composable const* child, ErrorStatus* error_status) const + Composable const* child, + ErrorStatus* error_status) const { auto parents = _path_from_child(child, error_status); if (is_error(error_status)) @@ -392,8 +403,8 @@ Composition::trimmed_range_of_child( auto new_duration = std::min( result_range->end_time_exclusive(), - source_range()->end_time_exclusive()) - - new_start_time; + source_range()->end_time_exclusive()) + - new_start_time; if (new_duration.value() < 0) { return nullopt; @@ -440,13 +451,15 @@ Composition::trim_child_range(TimeRange child_range) const if (child_range.start_time() < sr.start_time()) { child_range = TimeRange::range_from_start_end_time( - sr.start_time(), child_range.end_time_exclusive()); + sr.start_time(), + child_range.end_time_exclusive()); } if (child_range.end_time_exclusive() > sr.end_time_exclusive()) { child_range = TimeRange::range_from_start_end_time( - child_range.start_time(), sr.end_time_exclusive()); + child_range.start_time(), + sr.end_time_exclusive()); } return child_range; @@ -532,7 +545,9 @@ Composition::child_at_time( } result = composition.value->child_at_time( - child_search_time, error_status, shallow_search); + child_search_time, + error_status, + shallow_search); if (is_error(error_status)) { return result; @@ -542,7 +557,8 @@ Composition::child_at_time( std::vector> Composition::children_in_range( - TimeRange const& search_range, ErrorStatus* error_status) const + TimeRange const& search_range, + ErrorStatus* error_status) const { std::vector> children; diff --git a/src/opentimelineio/composition.h b/src/opentimelineio/composition.h index 1de57e497..755280a72 100644 --- a/src/opentimelineio/composition.h +++ b/src/opentimelineio/composition.h @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Contributors to the OpenTimelineIO project + #pragma once #include "opentimelineio/item.h" @@ -38,10 +41,14 @@ class Composition : public Item ErrorStatus* error_status = nullptr); bool insert_child( - int index, Composable* child, ErrorStatus* error_status = nullptr); + int index, + Composable* child, + ErrorStatus* error_status = nullptr); bool set_child( - int index, Composable* child, ErrorStatus* error_status = nullptr); + int index, + Composable* child, + ErrorStatus* error_status = nullptr); bool remove_child(int index, ErrorStatus* error_status = nullptr); @@ -54,18 +61,23 @@ class Composition : public Item virtual std::pair, optional> handles_of_child( - Composable const* child, ErrorStatus* error_status = nullptr) const; + Composable const* child, + ErrorStatus* error_status = nullptr) const; virtual TimeRange range_of_child_at_index( - int index, ErrorStatus* error_status = nullptr) const; + int index, + ErrorStatus* error_status = nullptr) const; virtual TimeRange trimmed_range_of_child_at_index( - int index, ErrorStatus* error_status = nullptr) const; + int index, + ErrorStatus* error_status = nullptr) const; // leaving out reference_space argument for now: TimeRange range_of_child( - Composable const* child, ErrorStatus* error_status = nullptr) const; + Composable const* child, + ErrorStatus* error_status = nullptr) const; optional trimmed_range_of_child( - Composable const* child, ErrorStatus* error_status = nullptr) const; + Composable const* child, + ErrorStatus* error_status = nullptr) const; optional trim_child_range(TimeRange child_range) const; @@ -89,13 +101,13 @@ class Composition : public Item TimeRange const& search_range, ErrorStatus* error_status = nullptr) const; - // Return a vector of all objects that match the given template type. + // Find child objects that match the given template type. // // An optional search_time may be provided to limit the search. // - // If shallow_search is false, will recurse into children. + // The search is recursive unless shallow_search is set to true. template - std::vector> children_if( + std::vector> find_children( ErrorStatus* error_status = nullptr, optional search_range = nullopt, bool shallow_search = false) const; @@ -103,13 +115,15 @@ class Composition : public Item protected: virtual ~Composition(); - virtual bool read_from(Reader&); - virtual void write_to(Writer&) const; + bool read_from(Reader&) override; + void write_to(Writer&) const override; int _index_of_child( - Composable const* child, ErrorStatus* error_status = nullptr) const; + Composable const* child, + ErrorStatus* error_status = nullptr) const; std::vector _path_from_child( - Composable const* child, ErrorStatus* error_status = nullptr) const; + Composable const* child, + ErrorStatus* error_status = nullptr) const; private: // XXX: python implementation is O(n^2) in number of children @@ -157,7 +171,7 @@ class Composition : public Item template inline std::vector> -Composition::children_if( +Composition::find_children( ErrorStatus* error_status, optional search_range, bool shallow_search) const @@ -194,15 +208,19 @@ Composition::children_if( if (search_range) { search_range = transformed_time_range( - *search_range, composition, error_status); + *search_range, + composition, + error_status); if (is_error(error_status)) { return out; } } - const auto valid_children = composition->children_if( - error_status, search_range, shallow_search); + const auto valid_children = composition->find_children( + error_status, + search_range, + shallow_search); if (is_error(error_status)) { return out; diff --git a/src/opentimelineio/deserialization.cpp b/src/opentimelineio/deserialization.cpp index 8437371af..68cf28f26 100644 --- a/src/opentimelineio/deserialization.cpp +++ b/src/opentimelineio/deserialization.cpp @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Contributors to the OpenTimelineIO project + #include "opentime/rationalTime.h" #include "opentime/timeRange.h" #include "opentime/timeTransform.h" @@ -228,7 +231,9 @@ class JSONDecoder : public OTIO_rapidjson:: _error_status = ErrorStatus( ErrorStatus::INTERNAL_ERROR, string_printf( - "%s (near line %d)", err_msg.c_str(), _line_number_function())); + "%s (near line %d)", + err_msg.c_str(), + _line_number_function())); } void _error(ErrorStatus const& error_status) @@ -334,7 +339,10 @@ SerializableObject::Reader::_fix_reference_ids( if (a.type() == typeid(AnyDictionary)) { _fix_reference_ids( - any_cast(a), error_function, resolver, line_number); + any_cast(a), + error_function, + resolver, + line_number); } else if (a.type() == typeid(AnyVector)) { @@ -342,7 +350,10 @@ SerializableObject::Reader::_fix_reference_ids( for (size_t i = 0; i < child_array.size(); i++) { _fix_reference_ids( - child_array[i], error_function, resolver, line_number); + child_array[i], + error_function, + resolver, + line_number); } } else if (a.type() == typeid(SerializableObject::ReferenceId)) @@ -365,7 +376,9 @@ SerializableObject::Reader::_fix_reference_ids( template bool SerializableObject::Reader::_fetch( - std::string const& key, T* dest, bool* had_null) + std::string const& key, + T* dest, + bool* had_null) { auto e = _dict.find(key); if (e == _dict.end()) @@ -475,7 +488,8 @@ SerializableObject::Reader::_fetch(std::string const& key, int64_t* dest) bool SerializableObject::Reader::_fetch( - std::string const& key, SerializableObject** dest) + std::string const& key, + SerializableObject** dest) { auto e = _dict.find(key); if (e == _dict.end()) @@ -508,7 +522,8 @@ SerializableObject::Reader::_fetch( bool SerializableObject::Reader::_type_check( - std::type_info const& wanted, std::type_info const& found) + std::type_info const& wanted, + std::type_info const& found) { if (wanted != found) { @@ -567,8 +582,8 @@ SerializableObject::Reader::_decode(_Resolver& resolver) else if (schema_name_and_version == "TimeRange.1") { RationalTime start_time, duration; - return _fetch("start_time", &start_time) && - _fetch("duration", &duration) + return _fetch("start_time", &start_time) + && _fetch("duration", &duration) ? any(TimeRange(start_time, duration)) : any(); } @@ -576,8 +591,8 @@ SerializableObject::Reader::_decode(_Resolver& resolver) { RationalTime offset; double rate, scale; - return _fetch("offset", &offset) && _fetch("rate", &rate) && - _fetch("scale", &scale) + return _fetch("offset", &offset) && _fetch("rate", &rate) + && _fetch("scale", &scale) ? any(TimeTransform(offset, scale, rate)) : any(); } @@ -594,14 +609,14 @@ SerializableObject::Reader::_decode(_Resolver& resolver) else if (schema_name_and_version == "V2d.1") { double x, y; - return _fetch("x", &x) && _fetch("y", &y) ? any(Imath::V2d(x, y)) + return _fetch("x", &x) && _fetch("y", &y) ? any(IMATH_NAMESPACE::V2d(x, y)) : any(); } else if (schema_name_and_version == "Box2d.1") { - Imath::V2d min, max; + IMATH_NAMESPACE::V2d min, max; return _fetch("min", &min) && _fetch("max", &max) - ? any(Imath::Box2d(std::move(min), std::move(max))) + ? any(IMATH_NAMESPACE::Box2d(std::move(min), std::move(max))) : any(); } else @@ -618,7 +633,8 @@ SerializableObject::Reader::_decode(_Resolver& resolver) if (e != resolver.object_for_id.end()) { _error(ErrorStatus( - ErrorStatus::DUPLICATE_OBJECT_REFERENCE, ref_id)); + ErrorStatus::DUPLICATE_OBJECT_REFERENCE, + ref_id)); return any(); } } @@ -628,7 +644,9 @@ SerializableObject::Reader::_decode(_Resolver& resolver) int schema_version; if (!split_schema_string( - schema_name_and_version, &schema_name, &schema_version)) + schema_name_and_version, + &schema_name, + &schema_version)) { _error(ErrorStatus( ErrorStatus::MALFORMED_SCHEMA, @@ -725,13 +743,13 @@ SerializableObject::Reader::read(std::string const& key, AnyVector* value) } bool -SerializableObject::Reader::read(std::string const& key, Imath::V2d* value) +SerializableObject::Reader::read(std::string const& key, IMATH_NAMESPACE::V2d* value) { return _fetch(key, value); } bool -SerializableObject::Reader::read(std::string const& key, Imath::Box2d* value) +SerializableObject::Reader::read(std::string const& key, IMATH_NAMESPACE::Box2d* value) { return _fetch(key, value); } @@ -739,7 +757,8 @@ SerializableObject::Reader::read(std::string const& key, Imath::Box2d* value) template bool SerializableObject::Reader::_read_optional( - std::string const& key, optional* value) + std::string const& key, + optional* value) { bool had_null; T result; @@ -765,35 +784,40 @@ SerializableObject::Reader::read(std::string const& key, optional* value) bool SerializableObject::Reader::read( - std::string const& key, optional* value) + std::string const& key, + optional* value) { return _read_optional(key, value); } bool SerializableObject::Reader::read( - std::string const& key, optional* value) + std::string const& key, + optional* value) { return _read_optional(key, value); } bool SerializableObject::Reader::read( - std::string const& key, optional* value) + std::string const& key, + optional* value) { return _read_optional(key, value); } bool SerializableObject::Reader::read( - std::string const& key, optional* value) + std::string const& key, + optional* value) { return _read_optional(key, value); } bool SerializableObject::Reader::read( - std::string const& key, optional* value) + std::string const& key, + optional* value) { return _read_optional(key, value); } @@ -817,7 +841,9 @@ SerializableObject::Reader::read(std::string const& key, any* value) bool deserialize_json_from_string( - std::string const& input, any* destination, ErrorStatus* error_status) + std::string const& input, + any* destination, + ErrorStatus* error_status) { OTIO_rapidjson::Reader reader; OTIO_rapidjson::StringStream ss(input.c_str()); @@ -856,7 +882,9 @@ deserialize_json_from_string( bool deserialize_json_from_file( - std::string const& file_name, any* destination, ErrorStatus* error_status) + std::string const& file_name, + any* destination, + ErrorStatus* error_status) { FILE* fp = nullptr; @@ -869,7 +897,7 @@ deserialize_json_from_file( { fp = nullptr; } -#else // _WINDOWS +#else // _WINDOWS fp = fopen(file_name.c_str(), "r"); #endif // _WINDOWS if (!fp) diff --git a/src/opentimelineio/deserialization.h b/src/opentimelineio/deserialization.h index 2be1884da..f010d0b6c 100644 --- a/src/opentimelineio/deserialization.h +++ b/src/opentimelineio/deserialization.h @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Contributors to the OpenTimelineIO project + #pragma once #include "opentimelineio/any.h" diff --git a/src/opentimelineio/effect.cpp b/src/opentimelineio/effect.cpp index 30e495fc2..e1e4f75f7 100644 --- a/src/opentimelineio/effect.cpp +++ b/src/opentimelineio/effect.cpp @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Contributors to the OpenTimelineIO project + #include "opentimelineio/effect.h" #include "opentimelineio/missingReference.h" @@ -17,8 +20,8 @@ Effect::~Effect() bool Effect::read_from(Reader& reader) { - return reader.read("effect_name", &_effect_name) && - Parent::read_from(reader); + return reader.read("effect_name", &_effect_name) + && Parent::read_from(reader); } void diff --git a/src/opentimelineio/effect.h b/src/opentimelineio/effect.h index 070e3767b..9c3908588 100644 --- a/src/opentimelineio/effect.h +++ b/src/opentimelineio/effect.h @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Contributors to the OpenTimelineIO project + #pragma once #include "opentimelineio/serializableObjectWithMetadata.h" @@ -31,8 +34,8 @@ class Effect : public SerializableObjectWithMetadata protected: virtual ~Effect(); - virtual bool read_from(Reader&); - virtual void write_to(Writer&) const; + bool read_from(Reader&) override; + void write_to(Writer&) const override; private: std::string _effect_name; diff --git a/src/opentimelineio/errorStatus.cpp b/src/opentimelineio/errorStatus.cpp index 685cbcded..96c7f8ea9 100644 --- a/src/opentimelineio/errorStatus.cpp +++ b/src/opentimelineio/errorStatus.cpp @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Contributors to the OpenTimelineIO project + #include "opentimelineio/errorStatus.h" namespace opentimelineio { namespace OPENTIMELINEIO_VERSION { @@ -60,7 +63,7 @@ ErrorStatus::outcome_to_string(Outcome o) case MEDIA_REFERENCES_DO_NOT_CONTAIN_ACTIVE_KEY: return "active key not found in media references"; case MEDIA_REFERENCES_CONTAIN_EMPTY_KEY: - return "the media referencess cannot contain an empty key"; + return "the media references cannot contain an empty key"; default: return "unknown/illegal ErrorStatus::Outcome code"; }; diff --git a/src/opentimelineio/errorStatus.h b/src/opentimelineio/errorStatus.h index 1c82889dd..0bbc82b68 100644 --- a/src/opentimelineio/errorStatus.h +++ b/src/opentimelineio/errorStatus.h @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Contributors to the OpenTimelineIO project + #pragma once #include "opentimelineio/version.h" diff --git a/src/opentimelineio/externalReference.cpp b/src/opentimelineio/externalReference.cpp index bc1ef7183..ba5cc6735 100644 --- a/src/opentimelineio/externalReference.cpp +++ b/src/opentimelineio/externalReference.cpp @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Contributors to the OpenTimelineIO project + #include "opentimelineio/externalReference.h" namespace opentimelineio { namespace OPENTIMELINEIO_VERSION { @@ -6,7 +9,7 @@ ExternalReference::ExternalReference( std::string const& target_url, optional const& available_range, AnyDictionary const& metadata, - optional const& available_image_bounds) + optional const& available_image_bounds) : Parent(std::string(), available_range, metadata, available_image_bounds) , _target_url(target_url) {} diff --git a/src/opentimelineio/externalReference.h b/src/opentimelineio/externalReference.h index a35dd69a0..15a98e756 100644 --- a/src/opentimelineio/externalReference.h +++ b/src/opentimelineio/externalReference.h @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Contributors to the OpenTimelineIO project + #pragma once #include "opentimelineio/mediaReference.h" @@ -20,7 +23,7 @@ class ExternalReference final : public MediaReference std::string const& target_url = std::string(), optional const& available_range = nullopt, AnyDictionary const& metadata = AnyDictionary(), - optional const& available_image_bounds = nullopt); + optional const& available_image_bounds = nullopt); std::string target_url() const noexcept { return _target_url; } @@ -32,8 +35,8 @@ class ExternalReference final : public MediaReference protected: virtual ~ExternalReference(); - virtual bool read_from(Reader&); - virtual void write_to(Writer&) const; + bool read_from(Reader&) override; + void write_to(Writer&) const override; private: std::string _target_url; diff --git a/src/opentimelineio/freezeFrame.cpp b/src/opentimelineio/freezeFrame.cpp index 0ebd59066..a24c2e4a5 100644 --- a/src/opentimelineio/freezeFrame.cpp +++ b/src/opentimelineio/freezeFrame.cpp @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Contributors to the OpenTimelineIO project + #include "opentimelineio/freezeFrame.h" namespace opentimelineio { namespace OPENTIMELINEIO_VERSION { diff --git a/src/opentimelineio/freezeFrame.h b/src/opentimelineio/freezeFrame.h index 322faed72..ca025e6bc 100644 --- a/src/opentimelineio/freezeFrame.h +++ b/src/opentimelineio/freezeFrame.h @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Contributors to the OpenTimelineIO project + #pragma once #include "opentimelineio/linearTimeWarp.h" @@ -22,8 +25,6 @@ class FreezeFrame : public LinearTimeWarp protected: virtual ~FreezeFrame(); - -private: }; }} // namespace opentimelineio::OPENTIMELINEIO_VERSION diff --git a/src/opentimelineio/gap.cpp b/src/opentimelineio/gap.cpp index 1340d9489..c7bb28b0a 100644 --- a/src/opentimelineio/gap.cpp +++ b/src/opentimelineio/gap.cpp @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Contributors to the OpenTimelineIO project + #include "opentimelineio/gap.h" namespace opentimelineio { namespace OPENTIMELINEIO_VERSION { @@ -18,11 +21,11 @@ Gap::Gap( std::vector const& markers, AnyDictionary const& metadata) : Parent( - name, - TimeRange(RationalTime(0, duration.rate()), duration), - metadata, - effects, - markers) + name, + TimeRange(RationalTime(0, duration.rate()), duration), + metadata, + effects, + markers) {} Gap::~Gap() diff --git a/src/opentimelineio/gap.h b/src/opentimelineio/gap.h index 77947e63f..e1d4cc7e3 100644 --- a/src/opentimelineio/gap.h +++ b/src/opentimelineio/gap.h @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Contributors to the OpenTimelineIO project + #pragma once #include "opentimelineio/item.h" @@ -29,15 +32,13 @@ class Gap : public Item std::vector const& markers = std::vector(), AnyDictionary const& metadata = AnyDictionary()); - virtual bool visible() const; + bool visible() const override; protected: virtual ~Gap(); - virtual bool read_from(Reader&); - virtual void write_to(Writer&) const; - -private: + bool read_from(Reader&) override; + void write_to(Writer&) const override; }; }} // namespace opentimelineio::OPENTIMELINEIO_VERSION diff --git a/src/opentimelineio/generatorReference.cpp b/src/opentimelineio/generatorReference.cpp index 9cb3b65f0..696a3d229 100644 --- a/src/opentimelineio/generatorReference.cpp +++ b/src/opentimelineio/generatorReference.cpp @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Contributors to the OpenTimelineIO project + #include "opentimelineio/generatorReference.h" namespace opentimelineio { namespace OPENTIMELINEIO_VERSION { @@ -8,7 +11,7 @@ GeneratorReference::GeneratorReference( optional const& available_range, AnyDictionary const& parameters, AnyDictionary const& metadata, - optional const& available_image_bounds) + optional const& available_image_bounds) : Parent(name, available_range, metadata, available_image_bounds) , _generator_kind(generator_kind) , _parameters(parameters) @@ -20,8 +23,9 @@ GeneratorReference::~GeneratorReference() bool GeneratorReference::read_from(Reader& reader) { - return reader.read("generator_kind", &_generator_kind) && - reader.read("parameters", &_parameters) && Parent::read_from(reader); + return reader.read("generator_kind", &_generator_kind) + && reader.read("parameters", &_parameters) + && Parent::read_from(reader); } void diff --git a/src/opentimelineio/generatorReference.h b/src/opentimelineio/generatorReference.h index af2be794b..0da3d1317 100644 --- a/src/opentimelineio/generatorReference.h +++ b/src/opentimelineio/generatorReference.h @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Contributors to the OpenTimelineIO project + #pragma once #include "opentimelineio/mediaReference.h" @@ -22,7 +25,7 @@ class GeneratorReference final : public MediaReference optional const& available_range = nullopt, AnyDictionary const& parameters = AnyDictionary(), AnyDictionary const& metadata = AnyDictionary(), - optional const& available_image_bounds = nullopt); + optional const& available_image_bounds = nullopt); std::string generator_kind() const noexcept { return _generator_kind; } @@ -38,8 +41,8 @@ class GeneratorReference final : public MediaReference protected: virtual ~GeneratorReference(); - virtual bool read_from(Reader&); - virtual void write_to(Writer&) const; + bool read_from(Reader&) override; + void write_to(Writer&) const override; private: std::string _generator_kind; diff --git a/src/opentimelineio/imageSequenceReference.cpp b/src/opentimelineio/imageSequenceReference.cpp index 368832326..16b590ed0 100644 --- a/src/opentimelineio/imageSequenceReference.cpp +++ b/src/opentimelineio/imageSequenceReference.cpp @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Contributors to the OpenTimelineIO project + #include "opentimelineio/imageSequenceReference.h" namespace opentimelineio { namespace OPENTIMELINEIO_VERSION { @@ -13,7 +16,7 @@ ImageSequenceReference::ImageSequenceReference( MissingFramePolicy const missing_frame_policy, optional const& available_range, AnyDictionary const& metadata, - optional const& available_image_bounds) + optional const& available_image_bounds) : Parent(std::string(), available_range, metadata, available_image_bounds) , _target_url_base(target_url_base) , _name_prefix(name_prefix) @@ -65,10 +68,11 @@ ImageSequenceReference::number_of_images_in_sequence() const int ImageSequenceReference::frame_for_time( - RationalTime const& time, ErrorStatus* error_status) const + RationalTime const& time, + ErrorStatus* error_status) const { - if (!this->available_range().has_value() || - !this->available_range().value().contains(time)) + if (!this->available_range().has_value() + || !this->available_range().value().contains(time)) { if (error_status) { @@ -91,7 +95,8 @@ ImageSequenceReference::frame_for_time( std::string ImageSequenceReference::target_url_for_image_number( - int image_number, ErrorStatus* error_status) const + int image_number, + ErrorStatus* error_status) const { if (_rate == 0) { @@ -104,8 +109,8 @@ ImageSequenceReference::target_url_for_image_number( return std::string(); } else if ( - !this->available_range().has_value() || - this->available_range().value().duration().value() == 0) + !this->available_range().has_value() + || this->available_range().value().duration().value() == 0) { if (error_status) { @@ -144,14 +149,14 @@ ImageSequenceReference::target_url_for_image_number( // If the base does not include a trailing slash, add it std::string path_sep = std::string(); const auto target_url_base_len = _target_url_base.length(); - if (target_url_base_len > 0 && - _target_url_base.compare(target_url_base_len - 1, 1, "/") != 0) + if (target_url_base_len > 0 + && _target_url_base.compare(target_url_base_len - 1, 1, "/") != 0) { path_sep = "/"; } - std::string out_string = _target_url_base + path_sep + _name_prefix + sign + - zero_pad + image_num_string + _name_suffix; + std::string out_string = _target_url_base + path_sep + _name_prefix + sign + + zero_pad + image_num_string + _name_suffix; if (error_status) { *error_status = ErrorStatus(ErrorStatus::OK); @@ -161,7 +166,8 @@ ImageSequenceReference::target_url_for_image_number( RationalTime ImageSequenceReference::presentation_time_for_image_number( - int image_number, ErrorStatus* error_status) const + int image_number, + ErrorStatus* error_status) const { if (image_number >= this->number_of_images_in_sequence()) { @@ -185,13 +191,14 @@ ImageSequenceReference::read_from(Reader& reader) int64_t frame_step_value = 0; int64_t frame_zero_padding_value = 0; - auto result = reader.read("target_url_base", &_target_url_base) && - reader.read("name_prefix", &_name_prefix) && - reader.read("name_suffix", &_name_suffix) && - reader.read("start_frame", &start_frame_value) && - reader.read("frame_step", &frame_step_value) && - reader.read("rate", &_rate) && - reader.read("frame_zero_padding", &frame_zero_padding_value); + auto result = + reader.read("target_url_base", &_target_url_base) + && reader.read("name_prefix", &_name_prefix) + && reader.read("name_suffix", &_name_suffix) + && reader.read("start_frame", &start_frame_value) + && reader.read("frame_step", &frame_step_value) + && reader.read("rate", &_rate) + && reader.read("frame_zero_padding", &frame_zero_padding_value); _start_frame = static_cast(start_frame_value); _frame_step = static_cast(frame_step_value); diff --git a/src/opentimelineio/imageSequenceReference.h b/src/opentimelineio/imageSequenceReference.h index 030346273..5694790ff 100644 --- a/src/opentimelineio/imageSequenceReference.h +++ b/src/opentimelineio/imageSequenceReference.h @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Contributors to the OpenTimelineIO project + #pragma once #include "opentimelineio/mediaReference.h" @@ -35,7 +38,7 @@ class ImageSequenceReference final : public MediaReference MissingFramePolicy::error, optional const& available_range = nullopt, AnyDictionary const& metadata = AnyDictionary(), - optional const& available_image_bounds = nullopt); + optional const& available_image_bounds = nullopt); std::string target_url_base() const noexcept { return _target_url_base; } @@ -94,19 +97,22 @@ class ImageSequenceReference final : public MediaReference int end_frame() const; int number_of_images_in_sequence() const; int frame_for_time( - RationalTime const& time, ErrorStatus* error_status = nullptr) const; + RationalTime const& time, + ErrorStatus* error_status = nullptr) const; std::string target_url_for_image_number( - int image_number, ErrorStatus* error_status = nullptr) const; + int image_number, + ErrorStatus* error_status = nullptr) const; RationalTime presentation_time_for_image_number( - int image_number, ErrorStatus* error_status = nullptr) const; + int image_number, + ErrorStatus* error_status = nullptr) const; protected: virtual ~ImageSequenceReference(); - virtual bool read_from(Reader&); - virtual void write_to(Writer&) const; + bool read_from(Reader&) override; + void write_to(Writer&) const override; private: std::string _target_url_base; diff --git a/src/opentimelineio/item.cpp b/src/opentimelineio/item.cpp index d5d608fa0..9ac6dfe43 100644 --- a/src/opentimelineio/item.cpp +++ b/src/opentimelineio/item.cpp @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Contributors to the OpenTimelineIO project + #include "opentimelineio/item.h" #include "opentimelineio/composition.h" #include "opentimelineio/effect.h" @@ -72,7 +75,8 @@ Item::visible_range(ErrorStatus* error_status) const if (head_tail.second) { result = TimeRange( - result.start_time(), result.duration() + *head_tail.second); + result.start_time(), + result.duration() + *head_tail.second); } } return result; @@ -104,7 +108,9 @@ Item::range_in_parent(ErrorStatus* error_status) const RationalTime Item::transformed_time( - RationalTime time, Item const* to_item, ErrorStatus* error_status) const + RationalTime time, + Item const* to_item, + ErrorStatus* error_status) const { if (!to_item) { @@ -154,7 +160,9 @@ Item::transformed_time( TimeRange Item::transformed_time_range( - TimeRange time_range, Item const* to_item, ErrorStatus* error_status) const + TimeRange time_range, + Item const* to_item, + ErrorStatus* error_status) const { return TimeRange( transformed_time(time_range.start_time(), to_item, error_status), @@ -164,11 +172,11 @@ Item::transformed_time_range( bool Item::read_from(Reader& reader) { - return reader.read_if_present("source_range", &_source_range) && - reader.read_if_present("effects", &_effects) && - reader.read_if_present("markers", &_markers) && - reader.read_if_present("enabled", &_enabled) && - Parent::read_from(reader); + return reader.read_if_present("source_range", &_source_range) + && reader.read_if_present("effects", &_effects) + && reader.read_if_present("markers", &_markers) + && reader.read_if_present("enabled", &_enabled) + && Parent::read_from(reader); } void diff --git a/src/opentimelineio/item.h b/src/opentimelineio/item.h index a2b07151a..570deacfa 100644 --- a/src/opentimelineio/item.h +++ b/src/opentimelineio/item.h @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Contributors to the OpenTimelineIO project + #pragma once #include "opentime/timeRange.h" @@ -30,8 +33,8 @@ class Item : public Composable std::vector const& markers = std::vector(), bool enabled = true); - virtual bool visible() const; - virtual bool overlapping() const; + bool visible() const override; + bool overlapping() const override; bool enabled() const { return _enabled; }; @@ -58,7 +61,7 @@ class Item : public Composable return _markers; } - virtual RationalTime duration(ErrorStatus* error_status = nullptr) const; + RationalTime duration(ErrorStatus* error_status = nullptr) const override; virtual TimeRange available_range(ErrorStatus* error_status = nullptr) const; @@ -88,8 +91,8 @@ class Item : public Composable protected: virtual ~Item(); - virtual bool read_from(Reader&); - virtual void write_to(Writer&) const; + bool read_from(Reader&) override; + void write_to(Writer&) const override; private: optional _source_range; diff --git a/src/opentimelineio/linearTimeWarp.cpp b/src/opentimelineio/linearTimeWarp.cpp index 20aaa0774..8700e3a99 100644 --- a/src/opentimelineio/linearTimeWarp.cpp +++ b/src/opentimelineio/linearTimeWarp.cpp @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Contributors to the OpenTimelineIO project + #include "opentimelineio/linearTimeWarp.h" namespace opentimelineio { namespace OPENTIMELINEIO_VERSION { @@ -17,8 +20,8 @@ LinearTimeWarp::~LinearTimeWarp() bool LinearTimeWarp::read_from(Reader& reader) { - return reader.read("time_scalar", &_time_scalar) && - Parent::read_from(reader); + return reader.read("time_scalar", &_time_scalar) + && Parent::read_from(reader); } void diff --git a/src/opentimelineio/linearTimeWarp.h b/src/opentimelineio/linearTimeWarp.h index 9cdbc7bd3..e4daaf13d 100644 --- a/src/opentimelineio/linearTimeWarp.h +++ b/src/opentimelineio/linearTimeWarp.h @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Contributors to the OpenTimelineIO project + #pragma once #include "opentimelineio/timeEffect.h" @@ -32,8 +35,8 @@ class LinearTimeWarp : public TimeEffect protected: virtual ~LinearTimeWarp(); - virtual bool read_from(Reader&); - virtual void write_to(Writer&) const; + bool read_from(Reader&) override; + void write_to(Writer&) const override; private: double _time_scalar; diff --git a/src/opentimelineio/marker.cpp b/src/opentimelineio/marker.cpp index d780e7800..0c6b748b1 100644 --- a/src/opentimelineio/marker.cpp +++ b/src/opentimelineio/marker.cpp @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Contributors to the OpenTimelineIO project + #include "opentimelineio/marker.h" #include "opentimelineio/missingReference.h" @@ -19,9 +22,9 @@ Marker::~Marker() bool Marker::read_from(Reader& reader) { - return reader.read_if_present("color", &_color) && - reader.read("marked_range", &_marked_range) && - Parent::read_from(reader); + return reader.read_if_present("color", &_color) + && reader.read("marked_range", &_marked_range) + && Parent::read_from(reader); } void diff --git a/src/opentimelineio/marker.h b/src/opentimelineio/marker.h index 82d40676b..927e9d647 100644 --- a/src/opentimelineio/marker.h +++ b/src/opentimelineio/marker.h @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Contributors to the OpenTimelineIO project + #pragma once #include "opentimelineio/serializableObjectWithMetadata.h" @@ -51,8 +54,8 @@ class Marker : public SerializableObjectWithMetadata protected: virtual ~Marker(); - virtual bool read_from(Reader&); - virtual void write_to(Writer&) const; + bool read_from(Reader&) override; + void write_to(Writer&) const override; private: std::string _color; diff --git a/src/opentimelineio/mediaReference.cpp b/src/opentimelineio/mediaReference.cpp index 9322264fc..40d8c4e88 100644 --- a/src/opentimelineio/mediaReference.cpp +++ b/src/opentimelineio/mediaReference.cpp @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Contributors to the OpenTimelineIO project + #include "opentimelineio/mediaReference.h" namespace opentimelineio { namespace OPENTIMELINEIO_VERSION { @@ -6,7 +9,7 @@ MediaReference::MediaReference( std::string const& name, optional const& available_range, AnyDictionary const& metadata, - optional const& available_image_bounds) + optional const& available_image_bounds) : Parent(name, metadata) , _available_range(available_range) , _available_image_bounds(available_image_bounds) @@ -24,10 +27,11 @@ MediaReference::is_missing_reference() const bool MediaReference::read_from(Reader& reader) { - return reader.read_if_present("available_range", &_available_range) && - reader.read_if_present( - "available_image_bounds", &_available_image_bounds) && - Parent::read_from(reader); + return reader.read_if_present("available_range", &_available_range) + && reader.read_if_present( + "available_image_bounds", + &_available_image_bounds) + && Parent::read_from(reader); } void diff --git a/src/opentimelineio/mediaReference.h b/src/opentimelineio/mediaReference.h index de6c09a83..716795337 100644 --- a/src/opentimelineio/mediaReference.h +++ b/src/opentimelineio/mediaReference.h @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Contributors to the OpenTimelineIO project + #pragma once #include "opentimelineio/serializableObjectWithMetadata.h" @@ -24,7 +27,7 @@ class MediaReference : public SerializableObjectWithMetadata std::string const& name = std::string(), optional const& available_range = nullopt, AnyDictionary const& metadata = AnyDictionary(), - optional const& available_image_bounds = nullopt); + optional const& available_image_bounds = nullopt); optional available_range() const noexcept { @@ -38,13 +41,13 @@ class MediaReference : public SerializableObjectWithMetadata virtual bool is_missing_reference() const; - optional available_image_bounds() const + optional available_image_bounds() const { return _available_image_bounds; } void set_available_image_bounds( - optional const& available_image_bounds) + optional const& available_image_bounds) { _available_image_bounds = available_image_bounds; } @@ -52,12 +55,12 @@ class MediaReference : public SerializableObjectWithMetadata protected: virtual ~MediaReference(); - virtual bool read_from(Reader&); - virtual void write_to(Writer&) const; + bool read_from(Reader&) override; + void write_to(Writer&) const override; private: optional _available_range; - optional _available_image_bounds; + optional _available_image_bounds; }; }} // namespace opentimelineio::OPENTIMELINEIO_VERSION diff --git a/src/opentimelineio/missingReference.cpp b/src/opentimelineio/missingReference.cpp index b51457063..82396f5a8 100644 --- a/src/opentimelineio/missingReference.cpp +++ b/src/opentimelineio/missingReference.cpp @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Contributors to the OpenTimelineIO project + #include "opentimelineio/missingReference.h" namespace opentimelineio { namespace OPENTIMELINEIO_VERSION { @@ -6,7 +9,7 @@ MissingReference::MissingReference( std::string const& name, optional const& available_range, AnyDictionary const& metadata, - optional const& available_image_bounds) + optional const& available_image_bounds) : Parent(name, available_range, metadata, available_image_bounds) {} diff --git a/src/opentimelineio/missingReference.h b/src/opentimelineio/missingReference.h index 898df23e9..2ad92e78e 100644 --- a/src/opentimelineio/missingReference.h +++ b/src/opentimelineio/missingReference.h @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Contributors to the OpenTimelineIO project + #pragma once #include "opentimelineio/mediaReference.h" @@ -20,15 +23,15 @@ class MissingReference final : public MediaReference std::string const& name = std::string(), optional const& available_range = nullopt, AnyDictionary const& metadata = AnyDictionary(), - optional const& available_image_bounds = nullopt); + optional const& available_image_bounds = nullopt); - virtual bool is_missing_reference() const; + bool is_missing_reference() const override; protected: virtual ~MissingReference(); - virtual bool read_from(Reader&); - virtual void write_to(Writer&) const; + bool read_from(Reader&) override; + void write_to(Writer&) const override; }; }} // namespace opentimelineio::OPENTIMELINEIO_VERSION diff --git a/src/opentimelineio/optional.h b/src/opentimelineio/optional.h index ee7469354..8b8521b04 100644 --- a/src/opentimelineio/optional.h +++ b/src/opentimelineio/optional.h @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Contributors to the OpenTimelineIO project + #pragma once #include "nonstd/optional.hpp" diff --git a/src/opentimelineio/safely_typed_any.cpp b/src/opentimelineio/safely_typed_any.cpp index 63f77e156..751429bb5 100644 --- a/src/opentimelineio/safely_typed_any.cpp +++ b/src/opentimelineio/safely_typed_any.cpp @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Contributors to the OpenTimelineIO project + #include "opentimelineio/safely_typed_any.h" namespace opentimelineio { namespace OPENTIMELINEIO_VERSION { @@ -57,13 +60,13 @@ create_safely_typed_any(TimeTransform&& value) } any -create_safely_typed_any(Imath::V2d&& value) +create_safely_typed_any(IMATH_NAMESPACE::V2d&& value) { return any(value); } any -create_safely_typed_any(Imath::Box2d&& value) +create_safely_typed_any(IMATH_NAMESPACE::Box2d&& value) { return any(value); } @@ -140,16 +143,16 @@ safely_cast_time_transform_any(any const& a) return any_cast(a); } -Imath::V2d +IMATH_NAMESPACE::V2d safely_cast_point_any(any const& a) { - return any_cast(a); + return any_cast(a); } -Imath::Box2d +IMATH_NAMESPACE::Box2d safely_cast_box_any(any const& a) { - return any_cast(a); + return any_cast(a); } AnyDictionary diff --git a/src/opentimelineio/safely_typed_any.h b/src/opentimelineio/safely_typed_any.h index d974ad7fa..535882acb 100644 --- a/src/opentimelineio/safely_typed_any.h +++ b/src/opentimelineio/safely_typed_any.h @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Contributors to the OpenTimelineIO project + #pragma once /** @@ -33,8 +36,8 @@ any create_safely_typed_any(std::string&&); any create_safely_typed_any(RationalTime&&); any create_safely_typed_any(TimeRange&&); any create_safely_typed_any(TimeTransform&&); -any create_safely_typed_any(Imath::V2d&&); -any create_safely_typed_any(Imath::Box2d&&); +any create_safely_typed_any(IMATH_NAMESPACE::V2d&&); +any create_safely_typed_any(IMATH_NAMESPACE::Box2d&&); any create_safely_typed_any(AnyVector&&); any create_safely_typed_any(AnyDictionary&&); any create_safely_typed_any(SerializableObject*); @@ -48,8 +51,8 @@ std::string safely_cast_string_any(any const& a); RationalTime safely_cast_rational_time_any(any const& a); TimeRange safely_cast_time_range_any(any const& a); TimeTransform safely_cast_time_transform_any(any const& a); -Imath::V2d safely_cast_point_any(any const& a); -Imath::Box2d safely_cast_box_any(any const& a); +IMATH_NAMESPACE::V2d safely_cast_point_any(any const& a); +IMATH_NAMESPACE::Box2d safely_cast_box_any(any const& a); SerializableObject* safely_cast_retainer_any(any const& a); diff --git a/src/opentimelineio/serializableCollection.cpp b/src/opentimelineio/serializableCollection.cpp index 8818e837a..def0014e5 100644 --- a/src/opentimelineio/serializableCollection.cpp +++ b/src/opentimelineio/serializableCollection.cpp @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Contributors to the OpenTimelineIO project + #include "opentimelineio/serializableCollection.h" #include "opentimelineio/clip.h" #include "opentimelineio/vectorIndexing.h" @@ -44,7 +47,9 @@ SerializableCollection::insert_child(int index, SerializableObject* child) bool SerializableCollection::set_child( - int index, SerializableObject* child, ErrorStatus* error_status) + int index, + SerializableObject* child, + ErrorStatus* error_status) { index = adjusted_vector_index(index, _children); if (index < 0 || index >= int(_children.size())) @@ -100,12 +105,12 @@ SerializableCollection::write_to(Writer& writer) const } std::vector> -SerializableCollection::clip_if( +SerializableCollection::find_clips( ErrorStatus* error_status, optional const& search_range, bool shallow_search) const { - return children_if(error_status, search_range, shallow_search); + return find_children(error_status, search_range, shallow_search); } }} // namespace opentimelineio::OPENTIMELINEIO_VERSION diff --git a/src/opentimelineio/serializableCollection.h b/src/opentimelineio/serializableCollection.h index 2afa02a85..f36ae2610 100644 --- a/src/opentimelineio/serializableCollection.h +++ b/src/opentimelineio/serializableCollection.h @@ -1,7 +1,11 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Contributors to the OpenTimelineIO project + #pragma once #include "opentimelineio/composition.h" #include "opentimelineio/serializableObjectWithMetadata.h" +#include "opentimelineio/timeline.h" #include "opentimelineio/version.h" namespace opentimelineio { namespace OPENTIMELINEIO_VERSION { @@ -48,23 +52,23 @@ class SerializableCollection : public SerializableObjectWithMetadata bool remove_child(int index, ErrorStatus* error_status = nullptr); - // Return a vector of clips. + // Find child clips. // // An optional search_range may be provided to limit the search. // - // If shallow_search is false, will recurse into children. - std::vector> clip_if( + // The search is recursive unless shallow_search is set to true. + std::vector> find_clips( ErrorStatus* error_status = nullptr, optional const& search_range = nullopt, bool shallow_search = false) const; - // Return a vector of all objects that match the given template type. + // Find child objects that match the given template type. // // An optional search_time may be provided to limit the search. // - // If shallow_search is false, will recurse into children. + // The search is recursive unless shallow_search is set to true. template - std::vector> children_if( + std::vector> find_children( ErrorStatus* error_status = nullptr, optional search_range = nullopt, bool shallow_search = false) const; @@ -72,8 +76,8 @@ class SerializableCollection : public SerializableObjectWithMetadata protected: virtual ~SerializableCollection(); - virtual bool read_from(Reader&); - virtual void write_to(Writer&) const; + bool read_from(Reader&) override; + void write_to(Writer&) const override; private: std::vector> _children; @@ -81,7 +85,7 @@ class SerializableCollection : public SerializableObjectWithMetadata template inline std::vector> -SerializableCollection::children_if( +SerializableCollection::find_children( ErrorStatus* error_status, optional search_range, bool shallow_search) const @@ -95,15 +99,15 @@ SerializableCollection::children_if( out.push_back(valid_child); } - // if not a shallow_search, for children that are serialiable collections or compositions, - // recurse into their children + // if not a shallow_search, for children that are serializable collections, + // compositions, or timelines, recurse into their children if (!shallow_search) { if (auto collection = dynamic_cast(child.value)) { const auto valid_children = - collection->children_if(error_status, search_range); + collection->find_children(error_status, search_range); if (is_error(error_status)) { return out; @@ -116,7 +120,20 @@ SerializableCollection::children_if( else if (auto composition = dynamic_cast(child.value)) { const auto valid_children = - composition->children_if(error_status, search_range); + composition->find_children(error_status, search_range); + if (is_error(error_status)) + { + return out; + } + for (const auto& valid_child: valid_children) + { + out.push_back(valid_child); + } + } + else if (auto timeline = dynamic_cast(child.value)) + { + const auto valid_children = + timeline->find_children(error_status, search_range); if (is_error(error_status)) { return out; diff --git a/src/opentimelineio/serializableObject.cpp b/src/opentimelineio/serializableObject.cpp index 4453aa3a6..dfad434a8 100644 --- a/src/opentimelineio/serializableObject.cpp +++ b/src/opentimelineio/serializableObject.cpp @@ -1,7 +1,11 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Contributors to the OpenTimelineIO project + #include "opentimelineio/serializableObject.h" #include "opentimelineio/deserialization.h" #include "opentimelineio/serialization.h" #include "stringUtils.h" +#include "typeRegistry.h" namespace opentimelineio { namespace OPENTIMELINEIO_VERSION { @@ -109,23 +113,37 @@ SerializableObject::is_unknown_schema() const } std::string -SerializableObject::to_json_string(ErrorStatus* error_status, int indent) const +SerializableObject::to_json_string( + ErrorStatus* error_status, + const schema_version_map* schema_version_targets, + int indent) const { return serialize_json_to_string( - any(Retainer<>(this)), error_status, indent); + any(Retainer<>(this)), + schema_version_targets, + error_status, + indent); } bool SerializableObject::to_json_file( - std::string const& file_name, ErrorStatus* error_status, int indent) const + std::string const& file_name, + ErrorStatus* error_status, + const schema_version_map* schema_version_targets, + int indent) const { return serialize_json_to_file( - any(Retainer<>(this)), file_name, error_status, indent); + any(Retainer<>(this)), + file_name, + schema_version_targets, + error_status, + indent); } SerializableObject* SerializableObject::from_json_string( - std::string const& input, ErrorStatus* error_status) + std::string const& input, + ErrorStatus* error_status) { any dest; @@ -152,7 +170,8 @@ SerializableObject::from_json_string( SerializableObject* SerializableObject::from_json_file( - std::string const& file_name, ErrorStatus* error_status) + std::string const& file_name, + ErrorStatus* error_status) { any dest; @@ -224,7 +243,8 @@ SerializableObject::_managed_release() void SerializableObject::install_external_keepalive_monitor( - std::function monitor, bool apply_now) + std::function monitor, + bool apply_now) { { std::lock_guard lock(_mutex); diff --git a/src/opentimelineio/serializableObject.h b/src/opentimelineio/serializableObject.h index 5309177fc..5d6f175bd 100644 --- a/src/opentimelineio/serializableObject.h +++ b/src/opentimelineio/serializableObject.h @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Contributors to the OpenTimelineIO project + #pragma once #include "opentime/rationalTime.h" @@ -11,12 +14,15 @@ #include "opentimelineio/version.h" #include "ImathBox.h" +#include "serialization.h" #include -#include +#include namespace opentimelineio { namespace OPENTIMELINEIO_VERSION { +class CloningEncoder; + class SerializableObject { public: @@ -32,7 +38,7 @@ class SerializableObject * You cannot directly delete a SerializableObject* (or, hopefully, anything * derived from it, as all derivations are required to protect the destructor). * - * Instead, call the member funtion possibly_delete(), which deletes the object + * Instead, call the member function possibly_delete(), which deletes the object * (and, recursively, the objects owned by this object), provided the objects * are not under external management (e.g. prevented from being deleted because an * external scripting system is holding a reference to them). @@ -40,16 +46,22 @@ class SerializableObject bool possibly_delete(); bool to_json_file( - std::string const& file_name, - ErrorStatus* error_status = nullptr, - int indent = 4) const; - std::string - to_json_string(ErrorStatus* error_status = nullptr, int indent = 4) const; + std::string const& file_name, + ErrorStatus* error_status = nullptr, + const schema_version_map* target_family_label_spec = nullptr, + int indent = 4) const; + + std::string to_json_string( + ErrorStatus* error_status = nullptr, + const schema_version_map* target_family_label_spec = nullptr, + int indent = 4) const; static SerializableObject* from_json_file( - std::string const& file_name, ErrorStatus* error_status = nullptr); + std::string const& file_name, + ErrorStatus* error_status = nullptr); static SerializableObject* from_json_string( - std::string const& input, ErrorStatus* error_status = nullptr); + std::string const& input, + ErrorStatus* error_status = nullptr); bool is_equivalent_to(SerializableObject const& other) const; @@ -60,7 +72,7 @@ class SerializableObject // is set appropriately. SerializableObject* clone(ErrorStatus* error_status = nullptr) const; - // Allow external system (e.g. Python, Swifft) to add serializable fields + // Allow external system (e.g. Python, Swift) to add serializable fields // on the fly. C++ implementations should have no need for this functionality. AnyDictionary& dynamic_fields() { return _dynamic_fields; } @@ -85,8 +97,8 @@ class SerializableObject bool read(std::string const& key, RationalTime* dest); bool read(std::string const& key, TimeRange* dest); bool read(std::string const& key, class TimeTransform* dest); - bool read(std::string const& key, Imath::V2d* value); - bool read(std::string const& key, Imath::Box2d* value); + bool read(std::string const& key, IMATH_NAMESPACE::V2d* value); + bool read(std::string const& key, IMATH_NAMESPACE::Box2d* value); bool read(std::string const& key, AnyVector* dest); bool read(std::string const& key, AnyDictionary* dest); bool read(std::string const& key, any* dest); @@ -97,7 +109,7 @@ class SerializableObject bool read(std::string const& key, optional* dest); bool read(std::string const& key, optional* dest); bool read(std::string const& key, optional* dest); - bool read(std::string const& key, optional* value); + bool read(std::string const& key, optional* value); // skipping std::string because we translate null into the empty // string, so the conversion is somewhat ambiguous @@ -137,10 +149,10 @@ class SerializableObject _error(ErrorStatus( ErrorStatus::TYPE_MISMATCH, std::string( - "Expected object of type " + - fwd_type_name_for_error_message(typeid(T)) + - "; read type " + fwd_type_name_for_error_message(so) + - " instead"))); + "Expected object of type " + + fwd_type_name_for_error_message(typeid(T)) + + "; read type " + fwd_type_name_for_error_message(so) + + " instead"))); return false; } @@ -179,7 +191,10 @@ class SerializableObject { int line_number = line_number_for_object[e.first]; Reader::_fix_reference_ids( - e.second, error_function, *this, line_number); + e.second, + error_function, + *this, + line_number); Reader r(e.second, error_function, e.first, line_number); e.first->read_from(r); } @@ -370,7 +385,7 @@ class SerializableObject _Resolver&, int line_number); - Reader(Reader const&) = delete; + Reader(Reader const&) = delete; Reader operator=(Reader const&) = delete; AnyDictionary _dict; @@ -389,9 +404,10 @@ class SerializableObject { public: static bool write_root( - any const& value, - class Encoder& encoder, - ErrorStatus* error_status = nullptr); + any const& value, + class Encoder& encoder, + const schema_version_map* downgrade_version_manifest = nullptr, + ErrorStatus* error_status = nullptr); void write(std::string const& key, bool value); void write(std::string const& key, int64_t value); @@ -399,11 +415,11 @@ class SerializableObject void write(std::string const& key, std::string const& value); void write(std::string const& key, RationalTime value); void write(std::string const& key, TimeRange value); - void write(std::string const& key, Imath::V2d value); - void write(std::string const& key, Imath::Box2d value); + void write(std::string const& key, IMATH_NAMESPACE::V2d value); + void write(std::string const& key, IMATH_NAMESPACE::Box2d value); void write(std::string const& key, optional value); void write(std::string const& key, optional value); - void write(std::string const& key, optional value); + void write(std::string const& key, optional value); void write(std::string const& key, class TimeTransform value); void write(std::string const& key, SerializableObject const* value); void write(std::string const& key, SerializableObject* value) @@ -437,7 +453,7 @@ class SerializableObject AnyVector av; av.reserve(value.size()); - for (auto e: value) + for (const auto& e: value) { av.emplace_back(_to_any(e)); } @@ -449,7 +465,7 @@ class SerializableObject static any _to_any(std::map const& value) { AnyDictionary am; - for (auto e: value) + for (const auto& e: value) { am.emplace(e.first, _to_any(e.second)); } @@ -463,7 +479,7 @@ class SerializableObject AnyVector av; av.reserve(value.size()); - for (auto e: value) + for (const auto& e: value) { av.emplace_back(_to_any(e)); } @@ -499,13 +515,19 @@ class SerializableObject } ///@} - Writer(class Encoder& encoder) + Writer( + class Encoder& encoder, + const schema_version_map* downgrade_version_manifest) : _encoder(encoder) + , _downgrade_version_manifest(downgrade_version_manifest) + { _build_dispatch_tables(); } - Writer(Writer const&) = delete; + ~Writer(); + + Writer(Writer const&) = delete; Writer operator=(Writer const&) = delete; void _build_dispatch_tables(); @@ -517,19 +539,26 @@ class SerializableObject bool _any_equals(any const& lhs, any const& rhs); std::string _no_key; - std::map> + std::unordered_map< + std::type_info const*, + std::function> _write_dispatch_table; - std::map< + std::unordered_map< std::type_info const*, std::function> _equality_dispatch_table; - std::map> + std::unordered_map> _write_dispatch_table_by_name; - std::map _id_for_object; - std::map _next_id_for_type; + std::unordered_map + _id_for_object; + std::unordered_map _next_id_for_type; + + Writer* _child_writer = nullptr; + CloningEncoder* _child_cloning_encoder = nullptr; - class Encoder& _encoder; + class Encoder& _encoder; + const schema_version_map* _downgrade_version_manifest; friend class SerializableObject; }; @@ -597,16 +626,17 @@ class SerializableObject protected: virtual ~SerializableObject(); + virtual bool _is_deletable(); + virtual std::string _schema_name_for_reference() const; + private: - SerializableObject(SerializableObject const&) = delete; + SerializableObject(SerializableObject const&) = delete; SerializableObject& operator=(SerializableObject const&) = delete; template friend struct Retainer; - virtual std::string _schema_name_for_reference() const; - void _managed_retain(); void _managed_release(); @@ -621,7 +651,8 @@ class SerializableObject }; void install_external_keepalive_monitor( - std::function monitor, bool apply_now); + std::function monitor, + bool apply_now); int current_ref_count() const; diff --git a/src/opentimelineio/serializableObjectWithMetadata.cpp b/src/opentimelineio/serializableObjectWithMetadata.cpp index f25d4c29d..d08d3b081 100644 --- a/src/opentimelineio/serializableObjectWithMetadata.cpp +++ b/src/opentimelineio/serializableObjectWithMetadata.cpp @@ -1,9 +1,13 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Contributors to the OpenTimelineIO project + #include "opentimelineio/serializableObjectWithMetadata.h" namespace opentimelineio { namespace OPENTIMELINEIO_VERSION { SerializableObjectWithMetadata::SerializableObjectWithMetadata( - std::string const& name, AnyDictionary const& metadata) + std::string const& name, + AnyDictionary const& metadata) : _name(name) , _metadata(metadata) {} @@ -14,9 +18,9 @@ SerializableObjectWithMetadata::~SerializableObjectWithMetadata() bool SerializableObjectWithMetadata::read_from(Reader& reader) { - return reader.read_if_present("metadata", &_metadata) && - reader.read_if_present("name", &_name) && - SerializableObject::read_from(reader); + return reader.read_if_present("metadata", &_metadata) + && reader.read_if_present("name", &_name) + && SerializableObject::read_from(reader); } void diff --git a/src/opentimelineio/serializableObjectWithMetadata.h b/src/opentimelineio/serializableObjectWithMetadata.h index cb15924e2..ddb33c128 100644 --- a/src/opentimelineio/serializableObjectWithMetadata.h +++ b/src/opentimelineio/serializableObjectWithMetadata.h @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Contributors to the OpenTimelineIO project + #pragma once #include "opentimelineio/serializableObject.h" @@ -29,9 +32,10 @@ class SerializableObjectWithMetadata : public SerializableObject AnyDictionary metadata() const noexcept { return _metadata; } protected: - ~SerializableObjectWithMetadata(); - virtual bool read_from(Reader&); - virtual void write_to(Writer&) const; + virtual ~SerializableObjectWithMetadata(); + + bool read_from(Reader&) override; + void write_to(Writer&) const override; private: std::string _name; diff --git a/src/opentimelineio/serialization.cpp b/src/opentimelineio/serialization.cpp index 581a0d4b2..e61bff3d4 100644 --- a/src/opentimelineio/serialization.cpp +++ b/src/opentimelineio/serialization.cpp @@ -1,6 +1,15 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Contributors to the OpenTimelineIO project + +#include "opentimelineio/serialization.h" +#include "errorStatus.h" +#include "nonstd/optional.hpp" +#include "opentimelineio/anyDictionary.h" #include "opentimelineio/serializableObject.h" #include "opentimelineio/unknownSchema.h" #include "stringUtils.h" +#include +#include #define RAPIDJSON_NAMESPACE OTIO_rapidjson #include @@ -48,6 +57,8 @@ class Encoder bool has_errored() { return is_error(_error_status); } + virtual bool encoding_to_anydict() { return false; } + virtual void start_object() = 0; virtual void end_object() = 0; @@ -66,7 +77,7 @@ class Encoder virtual void write_value(class TimeRange const& value) = 0; virtual void write_value(class TimeTransform const& value) = 0; virtual void write_value(struct SerializableObject::ReferenceId) = 0; - virtual void write_value(Imath::Box2d const&) = 0; + virtual void write_value(IMATH_NAMESPACE::Box2d const&) = 0; protected: void _error(ErrorStatus const& error_status) @@ -80,23 +91,38 @@ class Encoder }; /** - * This encoder builds up a dictionary as its method of "encoding". + * This encoder builds up a AnyDictionary as its method of "encoding". * The dictionary is than handed off to a CloningDecoder, to complete * copying of a SerializableObject instance. */ class CloningEncoder : public Encoder { public: - CloningEncoder(bool actually_clone) + enum class ResultObjectPolicy + { + CloneBackToSerializableObject = 0, + MathTypesConcreteAnyDictionaryResult, + OnlyAnyDictionary, + }; + + CloningEncoder( + CloningEncoder::ResultObjectPolicy result_object_policy, + const schema_version_map* schema_version_targets = nullptr) + : _result_object_policy(result_object_policy) + , _downgrade_version_manifest(schema_version_targets) { using namespace std::placeholders; _error_function = std::bind(&CloningEncoder::_error, this, _1); - _actually_clone = actually_clone; } virtual ~CloningEncoder() {} - void write_key(std::string const& key) + virtual bool encoding_to_anydict() override + { + return (_result_object_policy == ResultObjectPolicy::OnlyAnyDictionary); + } + + void write_key(std::string const& key) override { if (has_errored()) { @@ -113,6 +139,34 @@ class CloningEncoder : public Encoder _stack.back().cur_key = key; } + void _replace_back(AnyDictionary&& a) + { + if (has_errored()) + { + return; + } + + if (_stack.size() == 1) + { + any newstack(std::move(a)); + _root.swap(newstack); + } + else + { + _stack.pop_back(); + auto& top = _stack.back(); + if (top.is_dict) + { + top.dict.emplace(top.cur_key, a); + } + else + { + any newstack(std::move(a)); + top.array.emplace_back(newstack); + } + } + } + void _store(any&& a) { if (has_errored()) @@ -138,36 +192,118 @@ class CloningEncoder : public Encoder } } - void write_null_value() { _store(any()); } - - void write_value(bool value) { _store(any(value)); } - - void write_value(int value) { _store(any(value)); } - - void write_value(int64_t value) { _store(any(value)); } - - void write_value(uint64_t value) { _store(any(value)); } - - void write_value(std::string const& value) { _store(any(value)); } - - void write_value(double value) { _store(any(value)); } + void write_null_value() override { _store(any()); } + void write_value(bool value) override { _store(any(value)); } + void write_value(int value) override { _store(any(value)); } + void write_value(int64_t value) override { _store(any(value)); } + void write_value(uint64_t value) override { _store(any(value)); } + void write_value(std::string const& value) override { _store(any(value)); } + void write_value(double value) override { _store(any(value)); } - void write_value(RationalTime const& value) { _store(any(value)); } - - void write_value(TimeRange const& value) { _store(any(value)); } - - void write_value(TimeTransform const& value) { _store(any(value)); } + void write_value(RationalTime const& value) override + { + if (_result_object_policy == ResultObjectPolicy::OnlyAnyDictionary) + { + AnyDictionary result = { + { "OTIO_SCHEMA", "RationalTime.1" }, + { "value", value.value() }, + { "rate", value.rate() }, + }; + _store(any(std::move(result))); + } + else + { + _store(any(value)); + } + } + void write_value(TimeRange const& value) override + { - void write_value(SerializableObject::ReferenceId value) + if (_result_object_policy == ResultObjectPolicy::OnlyAnyDictionary) + { + AnyDictionary result = { + { "OTIO_SCHEMA", "TimeRange.1" }, + { "duration", value.duration() }, + { "start_time", value.start_time() }, + }; + _store(any(std::move(result))); + } + else + { + _store(any(value)); + } + } + void write_value(TimeTransform const& value) override + { + if (_result_object_policy == ResultObjectPolicy::OnlyAnyDictionary) + { + AnyDictionary result{ + { "OTIO_SCHEMA", "TimeTransform.1" }, + { "offset", value.offset() }, + { "rate", value.rate() }, + { "scale", value.scale() }, + }; + _store(any(std::move(result))); + } + else + { + _store(any(value)); + } + } + void write_value(SerializableObject::ReferenceId value) override { + if (_result_object_policy == ResultObjectPolicy::OnlyAnyDictionary) + { + AnyDictionary result{ + { "OTIO_SCHEMA", "SerializableObjectRef.1" }, + { "id", value.id.c_str() }, + }; + _store(any(std::move(result))); + } + else + { + _store(any(value)); + } _store(any(value)); } - void write_value(Imath::V2d const& value) { _store(any(value)); } + void write_value(IMATH_NAMESPACE::V2d const& value) + { - void write_value(Imath::Box2d const& value) { _store(any(value)); } + if (_result_object_policy == ResultObjectPolicy::OnlyAnyDictionary) + { + AnyDictionary result{ + { "OTIO_SCHEMA", "V2d.1" }, + { "x", value.x }, + { "y", value.y }, + }; + _store(any(std::move(result))); + } + else + { + _store(any(value)); + } + } - void start_array(size_t /* n */) + void write_value(IMATH_NAMESPACE::Box2d const& value) override + { + if (_result_object_policy == ResultObjectPolicy::OnlyAnyDictionary) + { + AnyDictionary result{ + { "OTIO_SCHEMA", "Box2d.1" }, + { "min", value.min }, + { "max", value.max }, + }; + _store(any(std::move(result))); + } + else + { + _store(any(value)); + } + } + // @} + + void start_array(size_t /* n */) override { if (has_errored()) { @@ -177,7 +313,7 @@ class CloningEncoder : public Encoder _stack.emplace_back(_DictOrArray{ false /* is_dict*/ }); } - void start_object() + void start_object() override { if (has_errored()) { @@ -187,7 +323,7 @@ class CloningEncoder : public Encoder _stack.emplace_back(_DictOrArray{ true /* is_dict*/ }); } - void end_array() + void end_array() override { if (has_errored()) { @@ -218,7 +354,7 @@ class CloningEncoder : public Encoder } } - void end_object() + void end_object() override { if (has_errored()) { @@ -229,37 +365,45 @@ class CloningEncoder : public Encoder { _internal_error( "Encoder::end_object() called without matching start_object()"); + return; } - else + + auto& top = _stack.back(); + if (!top.is_dict) { - auto& top = _stack.back(); - if (!top.is_dict) - { - _internal_error( - "Encoder::end_object() called without matching start_object()"); - _stack.pop_back(); - } - else - { - /* - * Convert back to SerializableObject* right here. - */ - if (_actually_clone) - { - SerializableObject::Reader reader( - top.dict, _error_function, nullptr); - _stack.pop_back(); - _store(reader._decode(_resolver)); - } - else - { - AnyDictionary m; - m.swap(top.dict); - _stack.pop_back(); - _store(any(std::move(m))); - } - } + _internal_error( + "Encoder::end_object() called without matching start_object()"); + _stack.pop_back(); + + return; + } + + /* + * Convert back to SerializableObject* right here. + */ + if (_result_object_policy + == ResultObjectPolicy::CloneBackToSerializableObject) + { + SerializableObject::Reader reader( + top.dict, + _error_function, + nullptr); + _stack.pop_back(); + _store(reader._decode(_resolver)); + + return; + } + + AnyDictionary m; + m.swap(top.dict); + + if ((_downgrade_version_manifest != nullptr) + && (!_downgrade_version_manifest->empty())) + { + _downgrade_dictionary(m); } + + _replace_back(std::move(m)); } private: @@ -284,7 +428,75 @@ class CloningEncoder : public Encoder friend class SerializableObject; std::vector<_DictOrArray> _stack; - bool _actually_clone; + ResultObjectPolicy _result_object_policy; + const schema_version_map* _downgrade_version_manifest = nullptr; + + void _downgrade_dictionary(AnyDictionary& m) + { + std::string schema_string = ""; + + if (!m.get_if_set("OTIO_SCHEMA", &schema_string)) + { + return; + } + + const auto sep = schema_string.rfind('.'); + const std::string& schema_name = schema_string.substr(0, sep); + + const auto dg_version_it = + _downgrade_version_manifest->find(schema_name); + + if (dg_version_it == _downgrade_version_manifest->end()) + { + return; + } + + const std::string& schema_vers = schema_string.substr(sep + 1); + int current_version = -1; + + if (!schema_vers.empty()) + { + current_version = std::stoi(schema_vers); + } + + // @TODO: is 0 a legitimate schema version? + if (current_version < 0) + { + _internal_error(string_printf( + "Could not parse version number from Schema" + " string: %s", + schema_string.c_str())); + return; + } + + const int target_version = static_cast(dg_version_it->second); + + const auto& type_rec = + (TypeRegistry::instance()._find_type_record(schema_name)); + + while (current_version > target_version) + { + const auto& next_dg_fn = + (type_rec->downgrade_functions.find(current_version)); + + if (next_dg_fn == type_rec->downgrade_functions.end()) + { + _internal_error(string_printf( + "No downgrader function available for " + "going from version %d to version %d.", + current_version, + target_version)); + return; + } + + // apply it + next_dg_fn->second(&m); + + current_version--; + } + + m["OTIO_SCHEMA"] = schema_name + "." + std::to_string(current_version); + } }; template @@ -377,7 +589,7 @@ class JSONEncoder : public Encoder _writer.EndObject(); } - void write_value(Imath::V2d const& value) + void write_value(IMATH_NAMESPACE::V2d const& value) { _writer.StartObject(); @@ -393,7 +605,7 @@ class JSONEncoder : public Encoder _writer.EndObject(); } - void write_value(Imath::Box2d const& value) + void write_value(IMATH_NAMESPACE::Box2d const& value) { _writer.StartObject(); @@ -425,8 +637,8 @@ template bool _simple_any_comparison(any const& lhs, any const& rhs) { - return lhs.type() == typeid(T) && rhs.type() == typeid(T) && - any_cast(lhs) == any_cast(rhs); + return lhs.type() == typeid(T) && rhs.type() == typeid(T) + && any_cast(lhs) == any_cast(rhs); } template <> @@ -440,9 +652,9 @@ template <> bool _simple_any_comparison(any const& lhs, any const& rhs) { - return lhs.type() == typeid(char const*) && - rhs.type() == typeid(char const*) && - !strcmp(any_cast(lhs), any_cast(rhs)); + return lhs.type() == typeid(char const*) + && rhs.type() == typeid(char const*) + && !strcmp(any_cast(lhs), any_cast(rhs)); } void @@ -451,6 +663,7 @@ SerializableObject::Writer::_build_dispatch_tables() /* * These are basically atomic writes to the encoder: */ + auto& wt = _write_dispatch_table; wt[&typeid(void)] = [this](any const&) { _encoder.write_null_value(); }; wt[&typeid(bool)] = [this](any const& value) { @@ -477,11 +690,11 @@ SerializableObject::Writer::_build_dispatch_tables() wt[&typeid(TimeTransform)] = [this](any const& value) { _encoder.write_value(any_cast(value)); }; - wt[&typeid(Imath::V2d)] = [this](any const& value) { - _encoder.write_value(any_cast(value)); + wt[&typeid(IMATH_NAMESPACE::V2d)] = [this](any const& value) { + _encoder.write_value(any_cast(value)); }; - wt[&typeid(Imath::Box2d)] = [this](any const& value) { - _encoder.write_value(any_cast(value)); + wt[&typeid(IMATH_NAMESPACE::Box2d)] = [this](any const& value) { + _encoder.write_value(any_cast(value)); }; /* @@ -503,7 +716,7 @@ SerializableObject::Writer::_build_dispatch_tables() * Install a backup table, using the actual type name as a key. * This is to deal with type aliasing across compilation units. */ - for (auto e: wt) + for (const auto& e: wt) { _write_dispatch_table_by_name[e.first->name()] = e.second; } @@ -520,8 +733,8 @@ SerializableObject::Writer::_build_dispatch_tables() et[&typeid(TimeTransform)] = &_simple_any_comparison; et[&typeid(SerializableObject::ReferenceId)] = &_simple_any_comparison; - et[&typeid(Imath::V2d)] = &_simple_any_comparison; - et[&typeid(Imath::Box2d)] = &_simple_any_comparison; + et[&typeid(IMATH_NAMESPACE::V2d)] = &_simple_any_comparison; + et[&typeid(IMATH_NAMESPACE::Box2d)] = &_simple_any_comparison; /* * These next recurse back through the Writer itself: @@ -537,8 +750,8 @@ SerializableObject::Writer::_build_dispatch_tables() bool SerializableObject::Writer::_any_dict_equals(any const& lhs, any const& rhs) { - if (lhs.type() != typeid(AnyDictionary) || - rhs.type() != typeid(AnyDictionary)) + if (lhs.type() != typeid(AnyDictionary) + || rhs.type() != typeid(AnyDictionary)) { return false; } @@ -548,15 +761,15 @@ SerializableObject::Writer::_any_dict_equals(any const& lhs, any const& rhs) auto r_it = rd.begin(); - for (auto l_it: ld) + for (const auto& l_it: ld) { if (r_it == rd.end()) { return false; } - if (l_it.first != r_it->first || - !_any_equals(l_it.second, r_it->second)) + if (l_it.first != r_it->first + || !_any_equals(l_it.second, r_it->second)) { return false; } @@ -601,9 +814,12 @@ SerializableObject::Writer::_any_equals(any const& lhs, any const& rhs) bool SerializableObject::Writer::write_root( - any const& value, Encoder& encoder, ErrorStatus* error_status) + any const& value, + Encoder& encoder, + const schema_version_map* schema_version_targets, + ErrorStatus* error_status) { - Writer w(encoder); + Writer w(encoder, schema_version_targets); w.write(w._no_key, value); return !encoder.has_errored(error_status); } @@ -640,7 +856,8 @@ SerializableObject::Writer::write(std::string const& key, double value) void SerializableObject::Writer::write( - std::string const& key, std::string const& value) + std::string const& key, + std::string const& value) { _encoder_write_key(key); _encoder.write_value(value); @@ -662,7 +879,8 @@ SerializableObject::Writer::write(std::string const& key, TimeRange value) void SerializableObject::Writer::write( - std::string const& key, optional value) + std::string const& key, + optional value) { _encoder_write_key(key); value ? _encoder.write_value(*value) : _encoder.write_null_value(); @@ -670,7 +888,8 @@ SerializableObject::Writer::write( void SerializableObject::Writer::write( - std::string const& key, optional value) + std::string const& key, + optional value) { _encoder_write_key(key); value ? _encoder.write_value(*value) : _encoder.write_null_value(); @@ -678,7 +897,8 @@ SerializableObject::Writer::write( void SerializableObject::Writer::write( - std::string const& key, optional value) + std::string const& key, + optional value) { _encoder_write_key(key); value ? _encoder.write_value(*value) : _encoder.write_null_value(); @@ -693,7 +913,8 @@ SerializableObject::Writer::write(std::string const& key, TimeTransform value) void SerializableObject::Writer::write( - std::string const& key, SerializableObject const* value) + std::string const& key, + SerializableObject const* value) { _encoder_write_key(key); if (!value) @@ -731,32 +952,99 @@ SerializableObject::Writer::write( _next_id_for_type[schema_type_name] = 0; } - std::string next_id = schema_type_name + "-" + - std::to_string(++_next_id_for_type[schema_type_name]); + std::string next_id = + schema_type_name + "-" + + std::to_string(++_next_id_for_type[schema_type_name]); _id_for_object[value] = next_id; - _encoder.start_object(); + // detect if downgrading needs to happen + const std::string& schema_name = value->schema_name(); + int schema_version = value->schema_version(); + + any downgraded = {}; + + // if there is a manifest & the encoder is not converting to AnyDictionary + if ((_downgrade_version_manifest != nullptr) + && (!_downgrade_version_manifest->empty()) + && (!_encoder.encoding_to_anydict())) + { + const auto& target_version_it = + _downgrade_version_manifest->find(schema_name); + + // ...and if that downgrade manifest specifies a target version for + // this schema + if (target_version_it != _downgrade_version_manifest->end()) + { + const int target_version = + static_cast(target_version_it->second); + + // and the current_version is greater than the target version + if (schema_version > target_version) + { + if (_child_writer == nullptr) + { + _child_cloning_encoder = new CloningEncoder( + CloningEncoder::ResultObjectPolicy::OnlyAnyDictionary, + _downgrade_version_manifest); + _child_writer = new Writer(*_child_cloning_encoder, {}); + } + else + { + _child_cloning_encoder->_stack.clear(); + } + + _child_writer->write(_child_writer->_no_key, value); + + if (_child_cloning_encoder->has_errored( + &_encoder._error_status)) + { + return; + } - _encoder.write_key("OTIO_SCHEMA"); + downgraded.swap(_child_cloning_encoder->_root); + schema_version = target_version; + } + } + } + std::string schema_str = ""; + + // if its an unknown schema, the schema name is computed from the + // _original_schema_name and _original_schema_version attributes if (UnknownSchema const* us = dynamic_cast(value)) { - _encoder.write_value(string_printf( - "%s.%d", - us->_original_schema_name.c_str(), - us->_original_schema_version)); + schema_str = + (us->_original_schema_name + "." + + std::to_string(us->_original_schema_version)); } else { - _encoder.write_value(string_printf( - "%s.%d", value->schema_name().c_str(), value->schema_version())); + // otherwise, use the schema_name and schema_version attributes + schema_str = schema_name + "." + std::to_string(schema_version); } + _encoder.start_object(); + #ifdef OTIO_INSTANCING_SUPPORT _encoder.write_key("OTIO_REF_ID"); _encoder.write_value(next_id); #endif - value->write_to(*this); + + // write the contents of the object to the encoder, either the downgraded + // anydictionary or the SerializableObject + if (!(downgraded.empty())) + { + for (const auto& kv: any_cast(downgraded)) + { + this->write(kv.first, kv.second); + } + } + else + { + _encoder.write_key("OTIO_SCHEMA"); + _encoder.write_value(schema_str); + value->write_to(*this); + } _encoder.end_object(); @@ -770,14 +1058,14 @@ SerializableObject::Writer::write( } void -SerializableObject::Writer::write(std::string const& key, Imath::V2d value) +SerializableObject::Writer::write(std::string const& key, IMATH_NAMESPACE::V2d value) { _encoder_write_key(key); _encoder.write_value(value); } void -SerializableObject::Writer::write(std::string const& key, Imath::Box2d value) +SerializableObject::Writer::write(std::string const& key, IMATH_NAMESPACE::Box2d value) { _encoder_write_key(key); _encoder.write_value(value); @@ -785,13 +1073,14 @@ SerializableObject::Writer::write(std::string const& key, Imath::Box2d value) void SerializableObject::Writer::write( - std::string const& key, AnyDictionary const& value) + std::string const& key, + AnyDictionary const& value) { _encoder_write_key(key); _encoder.start_object(); - for (auto e: value) + for (const auto& e: value) { write(e.first, e.second); } @@ -801,13 +1090,14 @@ SerializableObject::Writer::write( void SerializableObject::Writer::write( - std::string const& key, AnyVector const& value) + std::string const& key, + AnyVector const& value) { _encoder_write_key(key); _encoder.start_array(value.size()); - for (auto e: value) + for (const auto& e: value) { write(_no_key, e); } @@ -826,19 +1116,19 @@ SerializableObject::Writer::write(std::string const& key, any const& value) if (e == _write_dispatch_table.end()) { /* - * Using the address of a type_info suffers from aliasing across compilation units. - * If we fail on a lookup, we fallback on the by_name table, but that's slow because - * we have to keep making a string each time. + * Using the address of a type_info suffers from aliasing across + * compilation units. If we fail on a lookup, we fallback on the + * by_name table, but that's slow because we have to keep making a + * string each time. * - * So when we fail, we insert the address of the type_info that failed to be found, - * so that we'll catch it the next time. This ensures we fail exactly once per alias - * per type while using this writer. + * So when we fail, we insert the address of the type_info that failed + * to be found, so that we'll catch it the next time. This ensures we + * fail exactly once per alias per type while using this writer. */ - auto backup_e = _write_dispatch_table_by_name.find(type.name()); + const auto& backup_e = _write_dispatch_table_by_name.find(type.name()); if (backup_e != _write_dispatch_table_by_name.end()) { - _write_dispatch_table[&type] = backup_e->second; - e = _write_dispatch_table.find(&type); + e = _write_dispatch_table.insert({ &type, backup_e->second }).first; } } @@ -850,10 +1140,9 @@ SerializableObject::Writer::write(std::string const& key, any const& value) { std::string s; std::string bad_type_name = - (type == typeid(UnknownType)) - ? type_name_for_error_message( - any_cast(value).type_name) - : type_name_for_error_message(type); + (type == typeid(UnknownType)) ? type_name_for_error_message( + any_cast(value).type_name) + : type_name_for_error_message(type); if (&key != &_no_key) { @@ -882,23 +1171,27 @@ SerializableObject::is_equivalent_to(SerializableObject const& other) const return false; } - CloningEncoder e1(false), e2(false); - SerializableObject::Writer w1(e1); - SerializableObject::Writer w2(e2); + const auto policy = (CloningEncoder::ResultObjectPolicy:: + MathTypesConcreteAnyDictionaryResult); + + CloningEncoder e1(policy), e2(policy); + SerializableObject::Writer w1(e1, {}); + SerializableObject::Writer w2(e2, {}); w1.write(w1._no_key, any(Retainer<>(this))); w2.write(w2._no_key, any(Retainer<>(&other))); return ( - !e1.has_errored() && !e2.has_errored() && - w1._any_equals(e1._root, e2._root)); + !e1.has_errored() && !e2.has_errored() + && w1._any_equals(e1._root, e2._root)); } SerializableObject* SerializableObject::clone(ErrorStatus* error_status) const { - CloningEncoder e(true /* actually_clone*/); - SerializableObject::Writer w(e); + CloningEncoder e( + CloningEncoder::ResultObjectPolicy::CloneBackToSerializableObject); + SerializableObject::Writer w(e, {}); w.write(w._no_key, any(Retainer<>(this))); if (e.has_errored(error_status)) @@ -921,68 +1214,112 @@ SerializableObject::clone(ErrorStatus* error_status) const : nullptr; } +// to json_string std::string -serialize_json_to_string( - any const& value, ErrorStatus* error_status, int indent) +serialize_json_to_string_pretty( + const any& value, + const schema_version_map* schema_version_targets, + ErrorStatus* error_status, + int indent) { - OTIO_rapidjson::StringBuffer s; + OTIO_rapidjson::StringBuffer output_string_buffer; + + OTIO_rapidjson::PrettyWriter< + decltype(output_string_buffer), + OTIO_rapidjson::UTF8<>, + OTIO_rapidjson::UTF8<>, + OTIO_rapidjson::CrtAllocator, + OTIO_rapidjson::kWriteNanAndInfFlag> + json_writer(output_string_buffer); - if (indent < 0) + json_writer.SetIndent(' ', indent); + + JSONEncoder json_encoder(json_writer); + + if (!SerializableObject::Writer::write_root( + value, + json_encoder, + schema_version_targets, + error_status)) { - OTIO_rapidjson::Writer< - decltype(s), - OTIO_rapidjson::UTF8<>, - OTIO_rapidjson::UTF8<>, - OTIO_rapidjson::CrtAllocator, - OTIO_rapidjson::kWriteNanAndInfFlag> - json_writer(s); - JSONEncoder json_encoder(json_writer); - - if (!SerializableObject::Writer::write_root( - value, json_encoder, error_status)) - { - return std::string(); - } + return std::string(); } - else - { - OTIO_rapidjson::PrettyWriter< - decltype(s), - OTIO_rapidjson::UTF8<>, - OTIO_rapidjson::UTF8<>, - OTIO_rapidjson::CrtAllocator, - OTIO_rapidjson::kWriteNanAndInfFlag> - json_writer(s); - JSONEncoder json_encoder(json_writer); + return std::string(output_string_buffer.GetString()); +} - json_writer.SetIndent(' ', indent); - if (!SerializableObject::Writer::write_root( - value, json_encoder, error_status)) - { - return std::string(); - } +// to json_string +std::string +serialize_json_to_string_compact( + const any& value, + const schema_version_map* schema_version_targets, + ErrorStatus* error_status) +{ + OTIO_rapidjson::StringBuffer output_string_buffer; + + OTIO_rapidjson::Writer< + decltype(output_string_buffer), + OTIO_rapidjson::UTF8<>, + OTIO_rapidjson::UTF8<>, + OTIO_rapidjson::CrtAllocator, + OTIO_rapidjson::kWriteNanAndInfFlag> + json_writer(output_string_buffer); + + JSONEncoder json_encoder(json_writer); + + if (!SerializableObject::Writer::write_root( + value, + json_encoder, + schema_version_targets, + error_status)) + { + return std::string(); } - return std::string(s.GetString()); + return std::string(output_string_buffer.GetString()); +} + +// to json_string +std::string +serialize_json_to_string( + const any& value, + const schema_version_map* schema_version_targets, + ErrorStatus* error_status, + int indent) +{ + if (indent > 0) + { + return serialize_json_to_string_pretty( + value, + schema_version_targets, + error_status, + indent); + } + return serialize_json_to_string_compact( + value, + schema_version_targets, + error_status); } bool serialize_json_to_file( - any const& value, - std::string const& file_name, - ErrorStatus* error_status, - int indent) + any const& value, + std::string const& file_name, + const schema_version_map* schema_version_targets, + ErrorStatus* error_status, + int indent) { + #if defined(_WINDOWS) const int wlen = MultiByteToWideChar(CP_UTF8, 0, file_name.c_str(), -1, NULL, 0); std::vector wchars(wlen); MultiByteToWideChar(CP_UTF8, 0, file_name.c_str(), -1, wchars.data(), wlen); std::ofstream os(wchars.data()); -#else // _WINDOWS +#else // _WINDOWS std::ofstream os(file_name); #endif // _WINDOWS + if (!os.is_open()) { if (error_status) @@ -996,36 +1333,39 @@ serialize_json_to_file( OTIO_rapidjson::OStreamWrapper osw(os); bool status; - if (indent < 0) - { - OTIO_rapidjson::Writer< - decltype(osw), - OTIO_rapidjson::UTF8<>, - OTIO_rapidjson::UTF8<>, - OTIO_rapidjson::CrtAllocator, - OTIO_rapidjson::kWriteNanAndInfFlag> - json_writer(osw); - JSONEncoder json_encoder(json_writer); - status = SerializableObject::Writer::write_root( - value, json_encoder, error_status); - } - else - { - OTIO_rapidjson::PrettyWriter< - decltype(osw), - OTIO_rapidjson::UTF8<>, - OTIO_rapidjson::UTF8<>, - OTIO_rapidjson::CrtAllocator, - OTIO_rapidjson::kWriteNanAndInfFlag> - json_writer(osw); - JSONEncoder json_encoder(json_writer); + OTIO_rapidjson::PrettyWriter< + decltype(osw), + OTIO_rapidjson::UTF8<>, + OTIO_rapidjson::UTF8<>, + OTIO_rapidjson::CrtAllocator, + OTIO_rapidjson::kWriteNanAndInfFlag> + json_writer(osw); + JSONEncoder json_encoder(json_writer); + if (indent >= 0) + { json_writer.SetIndent(' ', indent); - status = SerializableObject::Writer::write_root( - value, json_encoder, error_status); } + status = SerializableObject::Writer::write_root( + value, + json_encoder, + schema_version_targets, + error_status); + return status; } +SerializableObject::Writer::~Writer() +{ + if (_child_writer) + { + delete _child_writer; + } + if (_child_cloning_encoder) + { + delete _child_cloning_encoder; + } +} + }} // namespace opentimelineio::OPENTIMELINEIO_VERSION diff --git a/src/opentimelineio/serialization.h b/src/opentimelineio/serialization.h index 5f2c9c4a0..2fce4dbf6 100644 --- a/src/opentimelineio/serialization.h +++ b/src/opentimelineio/serialization.h @@ -1,20 +1,30 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Contributors to the OpenTimelineIO project + #pragma once #include "opentimelineio/any.h" #include "opentimelineio/errorStatus.h" +#include "opentimelineio/typeRegistry.h" #include "opentimelineio/version.h" +#include #include +#include namespace opentimelineio { namespace OPENTIMELINEIO_VERSION { std::string serialize_json_to_string( - const any& value, ErrorStatus* error_status = nullptr, int indent = 4); + const any& value, + const schema_version_map* schema_version_targets = nullptr, + ErrorStatus* error_status = nullptr, + int indent = 4); bool serialize_json_to_file( - const any& value, - std::string const& file_name, - ErrorStatus* error_status = nullptr, - int indent = 4); + const any& value, + std::string const& file_name, + const schema_version_map* schema_version_targets = nullptr, + ErrorStatus* error_status = nullptr, + int indent = 4); }} // namespace opentimelineio::OPENTIMELINEIO_VERSION diff --git a/src/opentimelineio/stack.cpp b/src/opentimelineio/stack.cpp index f5d00ec92..4f92876a5 100644 --- a/src/opentimelineio/stack.cpp +++ b/src/opentimelineio/stack.cpp @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Contributors to the OpenTimelineIO project + #include "opentimelineio/stack.h" #include "opentimelineio/clip.h" #include "opentimelineio/vectorIndexing.h" @@ -77,8 +80,8 @@ Stack::range_of_all_children(ErrorStatus* error_status) const } TimeRange -Stack::trimmed_range_of_child_at_index( - int index, ErrorStatus* error_status) const +Stack::trimmed_range_of_child_at_index(int index, ErrorStatus* error_status) + const { auto range = range_of_child_at_index(index, error_status); if (is_error(error_status) || !source_range()) @@ -88,7 +91,8 @@ Stack::trimmed_range_of_child_at_index( const TimeRange sr = *source_range(); return TimeRange( - sr.start_time(), std::min(range.duration(), sr.duration())); + sr.start_time(), + std::min(range.duration(), sr.duration())); } TimeRange @@ -110,29 +114,29 @@ Stack::available_range(ErrorStatus* error_status) const } std::vector> -Stack::clip_if( +Stack::find_clips( ErrorStatus* error_status, optional const& search_range, bool shallow_search) const { - return children_if(error_status, search_range, shallow_search); + return find_children(error_status, search_range, shallow_search); } -optional +optional Stack::available_image_bounds(ErrorStatus* error_status) const { - optional box; + optional box; bool found_first_child = false; - for (auto clip: children_if(error_status)) + for (auto clip: find_children(error_status)) { - optional child_box; + optional child_box; if (auto clip_box = clip->available_image_bounds(error_status)) { child_box = clip_box; } if (is_error(error_status)) { - return optional(); + return optional(); } if (child_box) { diff --git a/src/opentimelineio/stack.h b/src/opentimelineio/stack.h index 031af0483..065274308 100644 --- a/src/opentimelineio/stack.h +++ b/src/opentimelineio/stack.h @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Contributors to the OpenTimelineIO project + #pragma once #include "opentimelineio/composition.h" @@ -25,23 +28,27 @@ class Stack : public Composition std::vector const& effects = std::vector(), std::vector const& markers = std::vector()); - virtual TimeRange range_of_child_at_index( - int index, ErrorStatus* error_status = nullptr) const; - virtual TimeRange trimmed_range_of_child_at_index( - int index, ErrorStatus* error_status = nullptr) const; - virtual TimeRange - available_range(ErrorStatus* error_status = nullptr) const; + TimeRange range_of_child_at_index( + int index, + ErrorStatus* error_status = nullptr) const override; + TimeRange trimmed_range_of_child_at_index( + int index, + ErrorStatus* error_status = nullptr) const override; + TimeRange + available_range(ErrorStatus* error_status = nullptr) const override; - virtual std::map - range_of_all_children(ErrorStatus* error_status = nullptr) const; + std::map + range_of_all_children(ErrorStatus* error_status = nullptr) const override; - optional - available_image_bounds(ErrorStatus* error_status) const; + optional + available_image_bounds(ErrorStatus* error_status) const override; - // Return a vector of clips. + // Find child clips. // // An optional search_range may be provided to limit the search. - std::vector> clip_if( + // + // The search is recursive unless shallow_search is set to true. + std::vector> find_clips( ErrorStatus* error_status = nullptr, optional const& search_range = nullopt, bool shallow_search = false) const; @@ -49,12 +56,10 @@ class Stack : public Composition protected: virtual ~Stack(); - virtual std::string composition_kind() const; - - virtual bool read_from(Reader&); - virtual void write_to(Writer&) const; + std::string composition_kind() const override; -private: + bool read_from(Reader&) override; + void write_to(Writer&) const override; }; }} // namespace opentimelineio::OPENTIMELINEIO_VERSION diff --git a/src/opentimelineio/stackAlgorithm.cpp b/src/opentimelineio/stackAlgorithm.cpp index c59e4e1fa..6800cbc2d 100644 --- a/src/opentimelineio/stackAlgorithm.cpp +++ b/src/opentimelineio/stackAlgorithm.cpp @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Contributors to the OpenTimelineIO project + #include "opentimelineio/stackAlgorithm.h" #include "opentimelineio/track.h" #include "opentimelineio/trackAlgorithm.h" @@ -48,7 +51,8 @@ _flatten_next_item( else { auto result = range_track_map.emplace( - track, track->range_of_all_children(error_status)); + track, + track->range_of_all_children(error_status)); if (is_error(error_status)) { return; @@ -152,7 +156,12 @@ flatten_stack(Stack* in_stack, ErrorStatus* error_status) RangeTrackMap range_track_map; _flatten_next_item( - range_track_map, flat_track, tracks, -1, nullopt, error_status); + range_track_map, + flat_track, + tracks, + -1, + nullopt, + error_status); return flat_track; } @@ -164,7 +173,12 @@ flatten_stack(std::vector const& tracks, ErrorStatus* error_status) RangeTrackMap range_track_map; _flatten_next_item( - range_track_map, flat_track, tracks, -1, nullopt, error_status); + range_track_map, + flat_track, + tracks, + -1, + nullopt, + error_status); return flat_track; } }} // namespace opentimelineio::OPENTIMELINEIO_VERSION diff --git a/src/opentimelineio/stackAlgorithm.h b/src/opentimelineio/stackAlgorithm.h index c10c85fde..7fb22af8a 100644 --- a/src/opentimelineio/stackAlgorithm.h +++ b/src/opentimelineio/stackAlgorithm.h @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Contributors to the OpenTimelineIO project + #pragma once #include "opentimelineio/stack.h" @@ -8,6 +11,7 @@ namespace opentimelineio { namespace OPENTIMELINEIO_VERSION { Track* flatten_stack(Stack* in_stack, ErrorStatus* error_status = nullptr); Track* flatten_stack( - std::vector const& tracks, ErrorStatus* error_status = nullptr); + std::vector const& tracks, + ErrorStatus* error_status = nullptr); }} // namespace opentimelineio::OPENTIMELINEIO_VERSION diff --git a/src/opentimelineio/stringUtils.cpp b/src/opentimelineio/stringUtils.cpp index e26e9b29f..f84d97765 100644 --- a/src/opentimelineio/stringUtils.cpp +++ b/src/opentimelineio/stringUtils.cpp @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Contributors to the OpenTimelineIO project + #include "opentimelineio/serializableObject.h" #if defined(__GNUC__) || defined(__clang__) # include @@ -15,7 +18,8 @@ cxxabi_type_name_for_error_mesage(const char* name) int status = -4; // some arbitrary value to eliminate the compiler warning std::unique_ptr res{ - abi::__cxa_demangle(name, NULL, NULL, &status), std::free + abi::__cxa_demangle(name, NULL, NULL, &status), + std::free }; return (status == 0) ? res.get() : name; diff --git a/src/opentimelineio/stringUtils.h b/src/opentimelineio/stringUtils.h index 64c1b0d9d..6630238a3 100644 --- a/src/opentimelineio/stringUtils.h +++ b/src/opentimelineio/stringUtils.h @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Contributors to the OpenTimelineIO project + #pragma once #include "opentime/stringPrintf.h" diff --git a/src/opentimelineio/timeEffect.cpp b/src/opentimelineio/timeEffect.cpp index f3675687b..4e9d65a60 100644 --- a/src/opentimelineio/timeEffect.cpp +++ b/src/opentimelineio/timeEffect.cpp @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Contributors to the OpenTimelineIO project + #include "opentimelineio/timeEffect.h" namespace opentimelineio { namespace OPENTIMELINEIO_VERSION { diff --git a/src/opentimelineio/timeEffect.h b/src/opentimelineio/timeEffect.h index e96e743aa..c90147090 100644 --- a/src/opentimelineio/timeEffect.h +++ b/src/opentimelineio/timeEffect.h @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Contributors to the OpenTimelineIO project + #pragma once #include "opentimelineio/effect.h" diff --git a/src/opentimelineio/timeline.cpp b/src/opentimelineio/timeline.cpp index 58232e122..0f1496c96 100644 --- a/src/opentimelineio/timeline.cpp +++ b/src/opentimelineio/timeline.cpp @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Contributors to the OpenTimelineIO project + #include "opentimelineio/timeline.h" #include "opentimelineio/clip.h" @@ -24,9 +27,9 @@ Timeline::set_tracks(Stack* stack) bool Timeline::read_from(Reader& reader) { - return reader.read("tracks", &_tracks) && - reader.read_if_present("global_start_time", &_global_start_time) && - Parent::read_from(reader); + return reader.read("tracks", &_tracks) + && reader.read_if_present("global_start_time", &_global_start_time) + && Parent::read_from(reader); } void @@ -72,12 +75,12 @@ Timeline::audio_tracks() const } std::vector> -Timeline::clip_if( +Timeline::find_clips( ErrorStatus* error_status, optional const& search_range, bool shallow_search) const { - return _tracks.value->clip_if(error_status, search_range, shallow_search); + return _tracks.value->find_clips(error_status, search_range, shallow_search); } }} // namespace opentimelineio::OPENTIMELINEIO_VERSION diff --git a/src/opentimelineio/timeline.h b/src/opentimelineio/timeline.h index e617825c8..2203812e2 100644 --- a/src/opentimelineio/timeline.h +++ b/src/opentimelineio/timeline.h @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Contributors to the OpenTimelineIO project + #pragma once #include "opentimelineio/serializableObjectWithMetadata.h" @@ -50,7 +53,8 @@ class Timeline : public SerializableObjectWithMetadata } TimeRange range_of_child( - Composable const* child, ErrorStatus* error_status = nullptr) const + Composable const* child, + ErrorStatus* error_status = nullptr) const { return _tracks.value->range_of_child(child, error_status); } @@ -58,26 +62,28 @@ class Timeline : public SerializableObjectWithMetadata std::vector audio_tracks() const; std::vector video_tracks() const; - // Return a vector of clips. + // Find child clips. // // An optional search_range may be provided to limit the search. - std::vector> clip_if( + // + // The search is recursive unless shallow_search is set to true. + std::vector> find_clips( ErrorStatus* error_status = nullptr, optional const& search_range = nullopt, bool shallow_search = false) const; - // Return a vector of all objects that match the given template type. + // Find child objects that match the given template type. // // An optional search_time may be provided to limit the search. // - // If shallow_search is false, will recurse into children. + // The search is recursive unless shallow_search is set to true. template - std::vector> children_if( + std::vector> find_children( ErrorStatus* error_status = nullptr, optional search_range = nullopt, bool shallow_search = false) const; - optional + optional available_image_bounds(ErrorStatus* error_status) const { return _tracks.value->available_image_bounds(error_status); @@ -86,8 +92,8 @@ class Timeline : public SerializableObjectWithMetadata protected: virtual ~Timeline(); - virtual bool read_from(Reader&); - virtual void write_to(Writer&) const; + bool read_from(Reader&) override; + void write_to(Writer&) const override; private: optional _global_start_time; @@ -96,13 +102,15 @@ class Timeline : public SerializableObjectWithMetadata template inline std::vector> -Timeline::children_if( +Timeline::find_children( ErrorStatus* error_status, optional search_range, bool shallow_search) const { - return _tracks.value->children_if( - error_status, search_range, shallow_search); + return _tracks.value->find_children( + error_status, + search_range, + shallow_search); } }} // namespace opentimelineio::OPENTIMELINEIO_VERSION diff --git a/src/opentimelineio/track.cpp b/src/opentimelineio/track.cpp index db02f8d5a..8488f036c 100644 --- a/src/opentimelineio/track.cpp +++ b/src/opentimelineio/track.cpp @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Contributors to the OpenTimelineIO project + #include "opentimelineio/track.h" #include "opentimelineio/clip.h" #include "opentimelineio/gap.h" @@ -38,30 +41,6 @@ Track::write_to(Writer& writer) const writer.write("kind", _kind); } -static RationalTime -_safe_duration(Composable* c, ErrorStatus* error_status) -{ - if (auto item = dynamic_cast(c)) - { - return item->duration(error_status); - } - else if (auto transition = dynamic_cast(c)) - { - return transition->duration(error_status); - } - else - { - if (error_status) - { - *error_status = ErrorStatus( - ErrorStatus::OBJECT_WITHOUT_DURATION, - "Cannot determine duration from this kind of object", - c); - } - return RationalTime(); - } -} - TimeRange Track::range_of_child_at_index(int index, ErrorStatus* error_status) const { @@ -76,7 +55,7 @@ Track::range_of_child_at_index(int index, ErrorStatus* error_status) const } Composable* child = children()[index]; - RationalTime child_duration = _safe_duration(child, error_status); + RationalTime child_duration = child->duration(error_status); if (is_error(error_status)) { return TimeRange(); @@ -89,7 +68,7 @@ Track::range_of_child_at_index(int index, ErrorStatus* error_status) const Composable* child2 = children()[i]; if (!child2->overlapping()) { - start_time += _safe_duration(children()[i], error_status); + start_time += children()[i]->duration(error_status); } if (is_error(error_status)) { @@ -106,8 +85,8 @@ Track::range_of_child_at_index(int index, ErrorStatus* error_status) const } TimeRange -Track::trimmed_range_of_child_at_index( - int index, ErrorStatus* error_status) const +Track::trimmed_range_of_child_at_index(int index, ErrorStatus* error_status) + const { auto child_range = range_of_child_at_index(index, error_status); if (is_error(error_status)) @@ -132,7 +111,7 @@ TimeRange Track::available_range(ErrorStatus* error_status) const { RationalTime duration; - for (auto child: children()) + for (const auto& child: children()) { if (auto item = dynamic_retainer_cast(child)) { @@ -162,8 +141,8 @@ Track::available_range(ErrorStatus* error_status) const } std::pair, optional> -Track::handles_of_child( - Composable const* child, ErrorStatus* error_status) const +Track::handles_of_child(Composable const* child, ErrorStatus* error_status) + const { optional head, tail; auto neighbors = neighbors_of(child, error_status); @@ -258,7 +237,7 @@ Track::range_of_all_children(ErrorStatus* error_status) const } RationalTime last_end_time(0, rate); - for (auto child: children()) + for (const auto& child: children()) { if (auto transition = dynamic_retainer_cast(child)) { @@ -269,7 +248,8 @@ Track::range_of_all_children(ErrorStatus* error_status) const else if (auto item = dynamic_retainer_cast(child)) { auto last_range = TimeRange( - last_end_time, item->trimmed_range(error_status).duration()); + last_end_time, + item->trimmed_range(error_status).duration()); result[child] = last_range; last_end_time = last_range.end_time_exclusive(); } @@ -284,20 +264,20 @@ Track::range_of_all_children(ErrorStatus* error_status) const } std::vector> -Track::clip_if( +Track::find_clips( ErrorStatus* error_status, optional const& search_range, bool shallow_search) const { - return children_if(error_status, search_range, shallow_search); + return find_children(error_status, search_range, shallow_search); } -optional +optional Track::available_image_bounds(ErrorStatus* error_status) const { - optional box; + optional box; bool found_first_clip = false; - for (auto child: children()) + for (const auto& child: children()) { if (auto clip = dynamic_cast(child.value)) { @@ -318,7 +298,7 @@ Track::available_image_bounds(ErrorStatus* error_status) const } if (is_error(error_status)) { - return optional(); + return optional(); } } } diff --git a/src/opentimelineio/track.h b/src/opentimelineio/track.h index 2a91f802d..17110ed61 100644 --- a/src/opentimelineio/track.h +++ b/src/opentimelineio/track.h @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Contributors to the OpenTimelineIO project + #pragma once #include "opentimelineio/composition.h" @@ -40,44 +43,48 @@ class Track : public Composition void set_kind(std::string const& kind) { _kind = kind; } - virtual TimeRange range_of_child_at_index( - int index, ErrorStatus* error_status = nullptr) const; - virtual TimeRange trimmed_range_of_child_at_index( - int index, ErrorStatus* error_status = nullptr) const; - virtual TimeRange - available_range(ErrorStatus* error_status = nullptr) const; + TimeRange range_of_child_at_index( + int index, + ErrorStatus* error_status = nullptr) const override; + TimeRange trimmed_range_of_child_at_index( + int index, + ErrorStatus* error_status = nullptr) const override; + TimeRange + available_range(ErrorStatus* error_status = nullptr) const override; - virtual std::pair, optional> + std::pair, optional> handles_of_child( - Composable const* child, ErrorStatus* error_status = nullptr) const; + Composable const* child, + ErrorStatus* error_status = nullptr) const override; std::pair, Retainer> neighbors_of( Composable const* item, ErrorStatus* error_status = nullptr, NeighborGapPolicy insert_gap = NeighborGapPolicy::never) const; - virtual std::map - range_of_all_children(ErrorStatus* error_status = nullptr) const; + std::map + range_of_all_children(ErrorStatus* error_status = nullptr) const override; - optional - available_image_bounds(ErrorStatus* error_status) const; + optional + available_image_bounds(ErrorStatus* error_status) const override; - // Return a vector of clips. + // Find child clips. // // An optional search_range may be provided to limit the search. // - // If shallow_search is false, will recurse into compositions. - std::vector> clip_if( + // The search is recursive unless shallow_search is set to true. + std::vector> find_clips( ErrorStatus* error_status = nullptr, optional const& search_range = nullopt, bool shallow_search = false) const; protected: virtual ~Track(); - virtual std::string composition_kind() const; - virtual bool read_from(Reader&); - virtual void write_to(Writer&) const; + std::string composition_kind() const override; + + bool read_from(Reader&) override; + void write_to(Writer&) const override; private: std::string _kind; diff --git a/src/opentimelineio/trackAlgorithm.cpp b/src/opentimelineio/trackAlgorithm.cpp index 1f1f6f8c4..4b6e6e5be 100644 --- a/src/opentimelineio/trackAlgorithm.cpp +++ b/src/opentimelineio/trackAlgorithm.cpp @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Contributors to the OpenTimelineIO project + #include "opentimelineio/trackAlgorithm.h" #include "opentimelineio/transition.h" @@ -5,7 +8,9 @@ namespace opentimelineio { namespace OPENTIMELINEIO_VERSION { Track* track_trimmed_to_range( - Track* in_track, TimeRange trim_range, ErrorStatus* error_status) + Track* in_track, + TimeRange trim_range, + ErrorStatus* error_status) { Track* new_track = dynamic_cast(in_track->clone(error_status)); if (is_error(error_status) || !new_track) @@ -20,7 +25,8 @@ track_trimmed_to_range( } std::vector children_copy( - new_track->children().begin(), new_track->children().end()); + new_track->children().begin(), + new_track->children().end()); for (size_t i = children_copy.size(); i--;) { diff --git a/src/opentimelineio/trackAlgorithm.h b/src/opentimelineio/trackAlgorithm.h index 420d2a91c..54ce6d35e 100644 --- a/src/opentimelineio/trackAlgorithm.h +++ b/src/opentimelineio/trackAlgorithm.h @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Contributors to the OpenTimelineIO project + #pragma once #include "opentimelineio/track.h" @@ -6,6 +9,8 @@ namespace opentimelineio { namespace OPENTIMELINEIO_VERSION { Track* track_trimmed_to_range( - Track* in_track, TimeRange trim_range, ErrorStatus* error_status = nullptr); + Track* in_track, + TimeRange trim_range, + ErrorStatus* error_status = nullptr); }} // namespace opentimelineio::OPENTIMELINEIO_VERSION diff --git a/src/opentimelineio/transition.cpp b/src/opentimelineio/transition.cpp index 137978057..63be6f76e 100644 --- a/src/opentimelineio/transition.cpp +++ b/src/opentimelineio/transition.cpp @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Contributors to the OpenTimelineIO project + #include "opentimelineio/transition.h" #include "opentimelineio/composition.h" @@ -27,10 +30,10 @@ Transition::overlapping() const bool Transition::read_from(Reader& reader) { - return reader.read("in_offset", &_in_offset) && - reader.read("out_offset", &_out_offset) && - reader.read("transition_type", &_transition_type) && - Parent::read_from(reader); + return reader.read("in_offset", &_in_offset) + && reader.read("out_offset", &_out_offset) + && reader.read("transition_type", &_transition_type) + && Parent::read_from(reader); } void diff --git a/src/opentimelineio/transition.h b/src/opentimelineio/transition.h index 407fc51ad..ead239962 100644 --- a/src/opentimelineio/transition.h +++ b/src/opentimelineio/transition.h @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Contributors to the OpenTimelineIO project + #pragma once #include "opentimelineio/composable.h" @@ -29,7 +32,7 @@ class Transition : public Composable RationalTime out_offset = RationalTime(), AnyDictionary const& metadata = AnyDictionary()); - virtual bool overlapping() const; + bool overlapping() const override; std::string transition_type() const noexcept { return _transition_type; } @@ -52,8 +55,7 @@ class Transition : public Composable _out_offset = out_offset; } - // XX is this virtual? - virtual RationalTime duration(ErrorStatus* error_status = nullptr) const; + RationalTime duration(ErrorStatus* error_status = nullptr) const override; optional range_in_parent(ErrorStatus* error_status = nullptr) const; @@ -64,8 +66,8 @@ class Transition : public Composable protected: virtual ~Transition(); - virtual bool read_from(Reader&); - virtual void write_to(Writer&) const; + bool read_from(Reader&) override; + void write_to(Writer&) const override; private: std::string _transition_type; diff --git a/src/opentimelineio/typeRegistry.cpp b/src/opentimelineio/typeRegistry.cpp index b93835019..725407949 100644 --- a/src/opentimelineio/typeRegistry.cpp +++ b/src/opentimelineio/typeRegistry.cpp @@ -1,5 +1,9 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Contributors to the OpenTimelineIO project + #include "opentimelineio/typeRegistry.h" +#include "anyDictionary.h" #include "opentimelineio/clip.h" #include "opentimelineio/composable.h" #include "opentimelineio/composition.h" @@ -27,8 +31,6 @@ #include #include -//#include -//#include namespace opentimelineio { namespace OPENTIMELINEIO_VERSION { @@ -74,7 +76,10 @@ TypeRegistry::TypeRegistry() register_type(); register_type(); register_type_from_existing_type( - "SerializeableCollection", 1, "SerializableCollection", nullptr); + "SerializeableCollection", + 1, + "SerializableCollection", + nullptr); register_type(); register_type(); @@ -110,6 +115,27 @@ TypeRegistry::TypeRegistry() d->erase("media_reference"); }); + + // 2->1 + register_downgrade_function(Clip::Schema::name, 2, [](AnyDictionary* d) { + AnyDictionary mrefs; + std::string active_rkey = ""; + + if (d->get_if_set("media_references", &mrefs)) + { + if (d->get_if_set("active_media_reference_key", &active_rkey)) + { + AnyDictionary active_ref; + if (mrefs.get_if_set(active_rkey, &active_ref)) + { + (*d)["media_reference"] = active_ref; + } + } + } + + d->erase("media_references"); + d->erase("active_media_reference_key"); + }); } bool @@ -122,6 +148,25 @@ TypeRegistry::register_type( { std::lock_guard lock(_registry_mutex); + // auto existing_tr = _find_type_record(schema_name); + // + // // if the exact type record has already been added (happens in unit tests + // // and re-setting manifest stuff) + // if (existing_tr) + // { + // if ( + // existing_tr->schema_name == schema_name + // && existing_tr->schema_version == schema_version + // && existing_tr->class_name == class_name + // && ( + // existing_tr->create.target() + // == create.target() + // ) + // ) { + // return true; + // } + // } + if (!_find_type_record(schema_name)) { _TypeRecord* r = @@ -148,16 +193,18 @@ TypeRegistry::register_type_from_existing_type( { if (!_find_type_record(schema_name)) { - _type_records[schema_name] = new _TypeRecord{ - r->schema_name, r->schema_version, r->class_name, r->create - }; + _type_records[schema_name] = new _TypeRecord{ r->schema_name, + r->schema_version, + r->class_name, + r->create }; return true; } if (error_status) { *error_status = ErrorStatus( - ErrorStatus::SCHEMA_ALREADY_REGISTERED, schema_name); + ErrorStatus::SCHEMA_ALREADY_REGISTERED, + schema_name); } return false; } @@ -184,12 +231,26 @@ TypeRegistry::register_upgrade_function( std::lock_guard lock(_registry_mutex); if (auto r = _find_type_record(schema_name)) { - if (r->upgrade_functions.find(version_to_upgrade_to) == - r->upgrade_functions.end()) - { - r->upgrade_functions[version_to_upgrade_to] = upgrade_function; - return true; - } + auto result = r->upgrade_functions.insert( + { version_to_upgrade_to, upgrade_function }); + return result.second; + } + + return false; +} + +bool +TypeRegistry::register_downgrade_function( + std::string const& schema_name, + int version_to_downgrade_from, + std::function downgrade_function) +{ + std::lock_guard lock(_registry_mutex); + if (auto r = _find_type_record(schema_name)) + { + auto result = r->downgrade_functions.insert( + { version_to_downgrade_from, downgrade_function }); + return result.second; } return false; @@ -247,10 +308,10 @@ TypeRegistry::_instance_from_schema( } else if (schema_version < type_record->schema_version) { - for (auto e: type_record->upgrade_functions) + for (const auto& e: type_record->upgrade_functions) { - if (schema_version <= e.first && - e.first <= type_record->schema_version) + if (schema_version <= e.first + && e.first <= type_record->schema_version) { e.second(&dict); } @@ -324,4 +385,16 @@ TypeRegistry::set_type_record( return false; } +void +TypeRegistry::type_version_map(schema_version_map& result) +{ + std::lock_guard lock(_registry_mutex); + + for (const auto& pair: _type_records) + { + const auto record_ptr = pair.second; + result[record_ptr->schema_name] = record_ptr->schema_version; + } +} + }} // namespace opentimelineio::OPENTIMELINEIO_VERSION diff --git a/src/opentimelineio/typeRegistry.h b/src/opentimelineio/typeRegistry.h index 21660e232..2f2e61854 100644 --- a/src/opentimelineio/typeRegistry.h +++ b/src/opentimelineio/typeRegistry.h @@ -1,19 +1,34 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Contributors to the OpenTimelineIO project + #pragma once #include "opentimelineio/any.h" #include "opentimelineio/errorStatus.h" #include "opentimelineio/version.h" + #include #include #include #include #include +#include namespace opentimelineio { namespace OPENTIMELINEIO_VERSION { class SerializableObject; +class Encoder; class AnyDictionary; +// typedefs for the schema downgrading system +// @TODO: should we make version an int64_t? That would match what we can +// serialize natively, since we only serialize 64 bit signed ints. +using schema_version_map = std::unordered_map; +using label_to_schema_version_map = + std::unordered_map; + +extern const label_to_schema_version_map CORE_VERSION_MAP; + class TypeRegistry { public: @@ -87,7 +102,29 @@ class TypeRegistry std::function upgrade_function) { return register_upgrade_function( - CLASS::schema_name, version_to_upgrade_to, upgrade_function); + CLASS::schema_name, + version_to_upgrade_to, + upgrade_function); + } + + /// Downgrade function from version_to_downgrade_from to + /// version_to_downgrade_from - 1 + bool register_downgrade_function( + std::string const& schema_name, + int version_to_downgrade_from, + std::function downgrade_function); + + /// Convenience API for C++ developers. See the documentation of the + /// non-templated register_downgrade_function() for details. + template + bool register_downgrade_function( + int version_to_upgrade_to, + std::function upgrade_function) + { + return register_downgrade_function( + CLASS::schema_name, + version_to_upgrade_to, + upgrade_function); } SerializableObject* instance_from_schema( @@ -110,10 +147,13 @@ class TypeRegistry std::string const& schema_name, ErrorStatus* error_status = nullptr); + // for inspecting the type registry, build a map of schema name to version + void type_version_map(schema_version_map& result); + private: TypeRegistry(); - TypeRegistry(TypeRegistry const&) = delete; + TypeRegistry(TypeRegistry const&) = delete; TypeRegistry& operator=(TypeRegistry const&) = delete; class _TypeRecord @@ -124,6 +164,7 @@ class TypeRegistry std::function create; std::map> upgrade_functions; + std::map> downgrade_functions; _TypeRecord( std::string _schema_name, @@ -141,6 +182,7 @@ class TypeRegistry friend class TypeRegistry; friend class SerializableObject; + friend class CloningEncoder; }; // helper functions for lookup @@ -167,6 +209,7 @@ class TypeRegistry std::map _type_records_by_type_name; friend class SerializableObject; + friend class CloningEncoder; }; }} // namespace opentimelineio::OPENTIMELINEIO_VERSION diff --git a/src/opentimelineio/unknownSchema.cpp b/src/opentimelineio/unknownSchema.cpp index 3d75c580d..5ee35bda2 100644 --- a/src/opentimelineio/unknownSchema.cpp +++ b/src/opentimelineio/unknownSchema.cpp @@ -1,9 +1,13 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Contributors to the OpenTimelineIO project + #include "opentimelineio/unknownSchema.h" namespace opentimelineio { namespace OPENTIMELINEIO_VERSION { UnknownSchema::UnknownSchema( - std::string const& original_schema_name, int original_schema_version) + std::string const& original_schema_name, + int original_schema_version) : _original_schema_name(original_schema_name) , _original_schema_version(original_schema_version) {} diff --git a/src/opentimelineio/unknownSchema.h b/src/opentimelineio/unknownSchema.h index 8373340bf..4ba11087e 100644 --- a/src/opentimelineio/unknownSchema.h +++ b/src/opentimelineio/unknownSchema.h @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Contributors to the OpenTimelineIO project + #pragma once #include "opentimelineio/serializableObject.h" @@ -15,7 +18,8 @@ class UnknownSchema : public SerializableObject }; UnknownSchema( - std::string const& original_schema_name, int original_schema_version); + std::string const& original_schema_name, + int original_schema_version); std::string original_schema_name() const noexcept { @@ -27,16 +31,17 @@ class UnknownSchema : public SerializableObject return _original_schema_version; } - virtual bool read_from(Reader&); - virtual void write_to(Writer&) const; + bool read_from(Reader&) override; + void write_to(Writer&) const override; - virtual bool is_unknown_schema() const; + bool is_unknown_schema() const override; -private: +protected: virtual ~UnknownSchema(); - virtual std::string _schema_name_for_reference() const; + std::string _schema_name_for_reference() const override; +private: std::string _original_schema_name; int _original_schema_version; diff --git a/src/opentimelineio/vectorIndexing.h b/src/opentimelineio/vectorIndexing.h index 772355570..f14b72242 100644 --- a/src/opentimelineio/vectorIndexing.h +++ b/src/opentimelineio/vectorIndexing.h @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Contributors to the OpenTimelineIO project + #pragma once #include "opentimelineio/version.h" diff --git a/src/opentimelineio/version.h b/src/opentimelineio/version.h index 3044ed9f7..9c8b8905a 100644 --- a/src/opentimelineio/version.h +++ b/src/opentimelineio/version.h @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Contributors to the OpenTimelineIO project + #pragma once #define OPENTIMELINEIO_VERSION v1_0 diff --git a/src/opentimelineview/__init__.py b/src/opentimelineview/__init__.py index c1bf6d653..34e050681 100644 --- a/src/opentimelineview/__init__.py +++ b/src/opentimelineview/__init__.py @@ -1,26 +1,5 @@ -# +# SPDX-License-Identifier: Apache-2.0 # Copyright Contributors to the OpenTimelineIO project -# -# Licensed under the Apache License, Version 2.0 (the "Apache License") -# with the following modification; you may not use this file except in -# compliance with the Apache License and the following modification to it: -# Section 6. Trademarks. is deleted and replaced with: -# -# 6. Trademarks. This License does not grant permission to use the trade -# names, trademarks, service marks, or product names of the Licensor -# and its affiliates, except as required to comply with Section 4(c) of -# the License and to reproduce the content of the NOTICE file. -# -# You may obtain a copy of the Apache License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the Apache License with the above modification is -# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -# KIND, either express or implied. See the Apache License for the specific -# language governing permissions and limitations under the Apache License. -# # flake8: noqa diff --git a/src/opentimelineview/console.py b/src/opentimelineview/console.py index 5bc079407..2c4b2ee84 100755 --- a/src/opentimelineview/console.py +++ b/src/opentimelineview/console.py @@ -1,27 +1,7 @@ #!/usr/bin/env python # +# SPDX-License-Identifier: Apache-2.0 # Copyright Contributors to the OpenTimelineIO project -# -# Licensed under the Apache License, Version 2.0 (the "Apache License") -# with the following modification; you may not use this file except in -# compliance with the Apache License and the following modification to it: -# Section 6. Trademarks. is deleted and replaced with: -# -# 6. Trademarks. This License does not grant permission to use the trade -# names, trademarks, service marks, or product names of the Licensor -# and its affiliates, except as required to comply with Section 4(c) of -# the License and to reproduce the content of the NOTICE file. -# -# You may obtain a copy of the Apache License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the Apache License with the above modification is -# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -# KIND, either express or implied. See the Apache License for the specific -# language governing permissions and limitations under the Apache License. -# """Simple otio viewer""" @@ -100,7 +80,7 @@ def _parsed_args(): class TimelineWidgetItem(QtWidgets.QListWidgetItem): def __init__(self, timeline, *args, **kwargs): - super(TimelineWidgetItem, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) self.timeline = timeline @@ -114,7 +94,7 @@ def __init__( *args, **kwargs ): - super(Main, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) self.adapter_argument_map = adapter_argument_map or {} self.media_linker = media_linker self.media_linker_argument_map = media_linker_argument_map @@ -189,14 +169,14 @@ def _file_load(self): extensions = otio.adapters.suffixes_with_defined_adapters(read=True) - extensions_string = ' '.join('*.{}'.format(x) for x in extensions) + extensions_string = ' '.join(f'*.{x}' for x in extensions) path = str( QtWidgets.QFileDialog.getOpenFileName( self, 'Open OpenTimelineIO', start_folder, - 'OTIO ({extensions})'.format(extensions=extensions_string) + f'OTIO ({extensions_string})' )[0] ) @@ -205,7 +185,7 @@ def _file_load(self): def load(self, path): self._current_file = path - self.setWindowTitle('OpenTimelineIO View: "{}"'.format(path)) + self.setWindowTitle(f'OpenTimelineIO View: "{path}"') self.details_widget.set_item(None) self.tracks_widget.clear() file_contents = otio.adapters.read_from_file( @@ -279,7 +259,7 @@ def center(self): self.move(frame_geo.topLeft()) def show(self): - super(Main, self).show() + super().show() self.timeline_widget.frame_all() diff --git a/src/opentimelineview/details_widget.py b/src/opentimelineview/details_widget.py index bba76e564..62d5768bc 100644 --- a/src/opentimelineview/details_widget.py +++ b/src/opentimelineview/details_widget.py @@ -1,26 +1,5 @@ -# +# SPDX-License-Identifier: Apache-2.0 # Copyright Contributors to the OpenTimelineIO project -# -# Licensed under the Apache License, Version 2.0 (the "Apache License") -# with the following modification; you may not use this file except in -# compliance with the Apache License and the following modification to it: -# Section 6. Trademarks. is deleted and replaced with: -# -# 6. Trademarks. This License does not grant permission to use the trade -# names, trademarks, service marks, or product names of the Licensor -# and its affiliates, except as required to comply with Section 4(c) of -# the License and to reproduce the content of the NOTICE file. -# -# You may obtain a copy of the Apache License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the Apache License with the above modification is -# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -# KIND, either express or implied. See the Apache License for the specific -# language governing permissions and limitations under the Apache License. -# try: from PySide6 import QtWidgets, QtGui, QtCore @@ -34,7 +13,7 @@ class Details(QtWidgets.QTextEdit): """Text widget with the JSON string of the specified OTIO object.""" def __init__(self, *args, **kwargs): - super(Details, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) self.setReadOnly(True) self.font = QtGui.QFontDatabase.systemFont( QtGui.QFontDatabase.FixedFont) @@ -65,7 +44,7 @@ def set_item(self, item): class OTIOSyntaxHighlighter(QtGui.QSyntaxHighlighter): def __init__(self, palette, parent=None): - super(OTIOSyntaxHighlighter, self).__init__(parent) + super().__init__(parent) self.punctuation_format = QtGui.QTextCharFormat() self.punctuation_format.setForeground(palette.link()) diff --git a/src/opentimelineview/ruler_widget.py b/src/opentimelineview/ruler_widget.py index e3b2a1c09..18c8ece54 100644 --- a/src/opentimelineview/ruler_widget.py +++ b/src/opentimelineview/ruler_widget.py @@ -1,26 +1,5 @@ -# +# SPDX-License-Identifier: Apache-2.0 # Copyright Contributors to the OpenTimelineIO project -# -# Licensed under the Apache License, Version 2.0 (the "Apache License") -# with the following modification; you may not use this file except in -# compliance with the Apache License and the following modification to it: -# Section 6. Trademarks. is deleted and replaced with: -# -# 6. Trademarks. This License does not grant permission to use the trade -# names, trademarks, service marks, or product names of the Licensor -# and its affiliates, except as required to comply with Section 4(c) of -# the License and to reproduce the content of the NOTICE file. -# -# You may obtain a copy of the Apache License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the Apache License with the above modification is -# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -# KIND, either express or implied. See the Apache License for the specific -# language governing permissions and limitations under the Apache License. -# try: from PySide6 import QtGui, QtCore, QtWidgets @@ -36,7 +15,7 @@ class FrameNumber(QtWidgets.QGraphicsRectItem): def __init__(self, text, position, *args, **kwargs): - super(FrameNumber, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) self.frameNumber = QtWidgets.QGraphicsSimpleTextItem(self) self.frameNumber.setText("%s" % text) self.setBrush( @@ -100,7 +79,7 @@ def __init__(self, height, composition, *args, **kwargs): poly.append(QtCore.QPointF(0, RULER_SIZE)) poly.append(QtCore.QPointF(-0.5 * RULER_SIZE, 0.5 * RULER_SIZE)) poly.append(QtCore.QPointF(-0.5 * RULER_SIZE, -0.5 * RULER_SIZE)) - super(Ruler, self).__init__(poly, *args, **kwargs) + super().__init__(poly, *args, **kwargs) # to retrieve tracks and its children self.composition = composition @@ -129,7 +108,7 @@ def __callback(): menu.addAction(label, __callback) menu.exec_(event.screenPos()) - super(Ruler, self).contextMenuEvent(event) + super().contextMenuEvent(event) def set_time_space_callback(self, time_space): self._time_space = time_space @@ -143,19 +122,19 @@ def mouseMoveEvent(self, mouse_event): track_widgets.MARKER_SIZE)) self.update_frame() - super(Ruler, self).mouseMoveEvent(mouse_event) + super().mouseMoveEvent(mouse_event) def mouseReleaseEvent(self, mouse_event): self.setSelected(False) - super(Ruler, self).mouseReleaseEvent(mouse_event) + super().mouseReleaseEvent(mouse_event) def hoverEnterEvent(self, event): self.setSelected(True) - super(Ruler, self).hoverEnterEvent(event) + super().hoverEnterEvent(event) def hoverLeaveEvent(self, event): self.setSelected(False) - super(Ruler, self).hoverLeaveEvent(event) + super().hoverLeaveEvent(event) def init(self): for track_item in self.composition.items(): @@ -182,7 +161,7 @@ def setParentItem(self, timeSlider): subclass in order to add the rule to the timeSlider item. ''' timeSlider.add_ruler(self) - super(Ruler, self).setParentItem(timeSlider) + super().setParentItem(timeSlider) def map_to_time_space(self, item): ''' @@ -195,10 +174,15 @@ def map_to_time_space(self, item): is_head = False is_tail = False f = "-?-" + ratio = -1.0 + width = float(item.rect().width()) - ratio = (self.x() - item.x() + - track_widgets.CURRENT_ZOOM_LEVEL * - track_widgets.TRACK_NAME_WIDGET_WIDTH) / float(item.rect().width()) + if width > 0.0: + ratio = (self.x() - item.x() + + track_widgets.CURRENT_ZOOM_LEVEL * + track_widgets.TRACK_NAME_WIDGET_WIDTH) / width + else: + print(f"Warning: zero width item: {item}.") # The 'if' condition should be : ratio < 0 or ration >= 1 # However, we are cheating in order to display the last frame of @@ -279,7 +263,7 @@ def snap(self, direction, scene_width): def paint(self, *args, **kwargs): new_args = [args[0], QtWidgets.QStyleOptionGraphicsItem()] + list(args[2:]) - super(Ruler, self).paint(*new_args, **kwargs) + super().paint(*new_args, **kwargs) def itemChange(self, change, value): if change == QtWidgets.QGraphicsItem.ItemSelectedHasChanged: @@ -287,7 +271,7 @@ def itemChange(self, change, value): QtGui.QColor(0, 255, 0, 255) if self.isSelected() else QtGui.QColor(0, 0, 0, 255) ) - return super(Ruler, self).itemChange(change, value) + return super().itemChange(change, value) def counteract_zoom(self, zoom_level=1.0): self.setTransform(QtGui.QTransform.fromScale(zoom_level, 1.0)) diff --git a/src/opentimelineview/settings.py b/src/opentimelineview/settings.py index 4915188c1..3df898601 100644 --- a/src/opentimelineview/settings.py +++ b/src/opentimelineview/settings.py @@ -1,3 +1,6 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright Contributors to the OpenTimelineIO project + VIEW_STYLESHEET = """ QMainWindow { background-color: rgb(27, 27, 27); diff --git a/src/opentimelineview/timeline_widget.py b/src/opentimelineview/timeline_widget.py index 2863db109..cfe2daff3 100644 --- a/src/opentimelineview/timeline_widget.py +++ b/src/opentimelineview/timeline_widget.py @@ -1,26 +1,5 @@ -# +# SPDX-License-Identifier: Apache-2.0 # Copyright Contributors to the OpenTimelineIO project -# -# Licensed under the Apache License, Version 2.0 (the "Apache License") -# with the following modification; you may not use this file except in -# compliance with the Apache License and the following modification to it: -# Section 6. Trademarks. is deleted and replaced with: -# -# 6. Trademarks. This License does not grant permission to use the trade -# names, trademarks, service marks, or product names of the Licensor -# and its affiliates, except as required to comply with Section 4(c) of -# the License and to reproduce the content of the NOTICE file. -# -# You may obtain a copy of the Apache License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the Apache License with the above modification is -# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -# KIND, either express or implied. See the Apache License for the specific -# language governing permissions and limitations under the Apache License. -# try: from PySide6 import QtGui, QtCore, QtWidgets @@ -119,7 +98,7 @@ def group_filters(bitmask): class CompositionWidget(QtWidgets.QGraphicsScene): def __init__(self, composition, *args, **kwargs): - super(CompositionWidget, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) self.composition = composition self.setBackgroundBrush(QtGui.QBrush(QtGui.QColor(33, 33, 33))) @@ -400,7 +379,7 @@ def _cache_tracks(self): ''' Create a doubly linked list to navigate from track to track: track->get_next_up & track->get_next_up - "map_to_wodget" : Create a map to retrieve the pyside widget from + "map_to_widget" : Create a map to retrieve the pyside widget from the otio item ''' data_cache = dict() @@ -460,11 +439,11 @@ class CompositionView(QtWidgets.QGraphicsView): selection_changed = QtCore.Signal(otio.core.SerializableObject) def __init__(self, stack, *args, **kwargs): - super(CompositionView, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) self.setResizeAnchor(QtWidgets.QGraphicsView.AnchorUnderMouse) self.setTransformationAnchor(QtWidgets.QGraphicsView.AnchorUnderMouse) self.setScene(CompositionWidget(stack, parent=self)) - self.setAlignment((QtCore.Qt.AlignLeft | QtCore.Qt.AlignTop)) + self.setAlignment(QtCore.Qt.AlignLeft | QtCore.Qt.AlignTop) self.setStyleSheet('border: 0px;') self.scene().selectionChanged.connect(self.parse_selection_change) self._navigation_filter = None @@ -491,10 +470,10 @@ def mousePressEvent(self, mouse_event): ) self.setInteractive(not modifiers == QtCore.Qt.AltModifier) - super(CompositionView, self).mousePressEvent(mouse_event) + super().mousePressEvent(mouse_event) def mouseReleaseEvent(self, mouse_event): - super(CompositionView, self).mouseReleaseEvent(mouse_event) + super().mouseReleaseEvent(mouse_event) self.setDragMode(QtWidgets.QGraphicsView.NoDrag) def wheelEvent(self, event): @@ -693,7 +672,7 @@ def _get_new_item(self, key_event, curSelectedItem): return newSelectedItem def keyPressEvent(self, key_event): - super(CompositionView, self).keyPressEvent(key_event) + super().keyPressEvent(key_event) self.setInteractive(True) # Remove ruler_widget.Ruler instance from selection @@ -785,7 +764,7 @@ class Timeline(QtWidgets.QTabWidget): navigationfilter_changed = QtCore.Signal(int) def __init__(self, *args, **kwargs): - super(Timeline, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) self.timeline = None self.setTabsClosable(True) @@ -806,7 +785,7 @@ def set_timeline(self, timeline): self.add_stack(timeline.tracks) def add_stack(self, stack): - """open a tab for the stack or go to it if already present""" + """Open a tab for the stack or go to it if already present""" # find the tab for the stack if the tab has already been opened tab_index = next( diff --git a/src/opentimelineview/track_widgets.py b/src/opentimelineview/track_widgets.py index 92871b11d..6648b2484 100644 --- a/src/opentimelineview/track_widgets.py +++ b/src/opentimelineview/track_widgets.py @@ -1,26 +1,5 @@ -# +# SPDX-License-Identifier: Apache-2.0 # Copyright Contributors to the OpenTimelineIO project -# -# Licensed under the Apache License, Version 2.0 (the "Apache License") -# with the following modification; you may not use this file except in -# compliance with the Apache License and the following modification to it: -# Section 6. Trademarks. is deleted and replaced with: -# -# 6. Trademarks. This License does not grant permission to use the trade -# names, trademarks, service marks, or product names of the Licensor -# and its affiliates, except as required to comply with Section 4(c) of -# the License and to reproduce the content of the NOTICE file. -# -# You may obtain a copy of the Apache License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the Apache License with the above modification is -# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -# KIND, either express or implied. See the Apache License for the specific -# language governing permissions and limitations under the Apache License. -# try: from PySide6 import QtGui, QtCore, QtWidgets @@ -43,12 +22,25 @@ TRACK_NAME_WIDGET_WIDTH = 100.0 SHORT_NAME_LENGTH = 7 CURRENT_ZOOM_LEVEL = 1.0 +MARKER_COLORS = { + otio.schema.MarkerColor.RED: (0xff, 0x00, 0x00, 0xff), + otio.schema.MarkerColor.PINK: (0xff, 0x70, 0x70, 0xff), + otio.schema.MarkerColor.ORANGE: (0xff, 0xa0, 0x00, 0xff), + otio.schema.MarkerColor.YELLOW: (0xff, 0xff, 0x00, 0xff), + otio.schema.MarkerColor.GREEN: (0x00, 0xff, 0x00, 0xff), + otio.schema.MarkerColor.CYAN: (0x00, 0xff, 0xff, 0xff), + otio.schema.MarkerColor.BLUE: (0x00, 0x00, 0xff, 0xff), + otio.schema.MarkerColor.PURPLE: (0xa0, 0x00, 0xd0, 0xff), + otio.schema.MarkerColor.MAGENTA: (0xff, 0x00, 0xff, 0xff), + otio.schema.MarkerColor.WHITE: (0xff, 0xff, 0xff, 0xff), + otio.schema.MarkerColor.BLACK: (0x00, 0x00, 0x00, 0xff) +} class BaseItem(QtWidgets.QGraphicsRectItem): def __init__(self, item, timeline_range, *args, **kwargs): - super(BaseItem, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) self.item = item self.timeline_range = timeline_range @@ -81,7 +73,7 @@ def __init__(self, item, timeline_range, *args, **kwargs): def paint(self, *args, **kwargs): new_args = [args[0], QtWidgets.QStyleOptionGraphicsItem()] + list(args[2:]) - super(BaseItem, self).paint(*new_args, **kwargs) + super().paint(*new_args, **kwargs) def itemChange(self, change, value): if change == QtWidgets.QGraphicsItem.ItemSelectedHasChanged: @@ -100,7 +92,7 @@ def itemChange(self, change, value): self.parentItem().zValue() - 1 ) - return super(BaseItem, self).itemChange(change, value) + return super().itemChange(change, value) def _add_markers(self): trimmed_range = self.item.trimmed_range() @@ -110,8 +102,7 @@ def _add_markers(self): if not trimmed_range.overlaps(marked_time): continue - # @TODO: set the marker color if its set from the OTIO object - marker = Marker(m, None) + marker = Marker(m) marker.setY(0.5 * MARKER_SIZE) marker.setX( ( @@ -235,7 +226,7 @@ def counteract_zoom(self, zoom_level=1.0): class GapItem(BaseItem): def __init__(self, *args, **kwargs): - super(GapItem, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) self.setBrush( QtGui.QBrush(QtGui.QColor(100, 100, 100, 255)) ) @@ -245,19 +236,18 @@ def __init__(self, *args, **kwargs): class TrackNameItem(BaseItem): def __init__(self, track, rect, *args, **kwargs): - super(TrackNameItem, self).__init__(None, None, rect, - *args, **kwargs) + super().__init__(None, None, rect, *args, **kwargs) self.track = track self.track_name = 'Track' if not track.name else track.name self.full_track_name = self.track_name if len(self.track_name) > SHORT_NAME_LENGTH: self.track_name = self.track_name[:SHORT_NAME_LENGTH] + '...' - self.source_name_label.setText(self.track_name + '\n({})'.format(track.kind)) + self.source_name_label.setText(self.track_name + f'\n({track.kind})') self.source_name_label.setY( (TRACK_HEIGHT - self.source_name_label.boundingRect().height()) / 2.0 ) - self.setToolTip('{} items'.format(len(track))) + self.setToolTip(f'{len(track)} items') self.track_widget = None self.name_toggle = False self.font = self.source_name_label.font() @@ -271,7 +261,7 @@ def __init__(self, track, rect, *args, **kwargs): ) def mouseDoubleClickEvent(self, event): - super(TrackNameItem, self).mouseDoubleClickEvent(event) + super().mouseDoubleClickEvent(event) if self.name_toggle: track_name_rect = QtCore.QRectF( 0, @@ -281,7 +271,7 @@ def mouseDoubleClickEvent(self, event): ) self.setRect(track_name_rect) self.source_name_label.setText( - self.track_name + '\n({})'.format(self.track.kind)) + self.track_name + f'\n({self.track.kind})') for widget in self.track_widget.widget_items: widget.current_x_offset = self.short_width self.name_toggle = False @@ -294,7 +284,7 @@ def mouseDoubleClickEvent(self, event): ) self.setRect(track_name_rect) self.source_name_label.setText( - self.full_track_name + '\n({})'.format(self.track.kind)) + self.full_track_name + f'\n({self.track.kind})') for widget in self.track_widget.widget_items: widget.current_x_offset = self.full_width self.name_toggle = True @@ -329,7 +319,7 @@ def counteract_zoom(self, zoom_level=1.0): class EffectItem(QtWidgets.QGraphicsRectItem): def __init__(self, item, rect, *args, **kwargs): - super(EffectItem, self).__init__(rect, *args, **kwargs) + super().__init__(rect, *args, **kwargs) self.item = item self.setFlags(QtWidgets.QGraphicsItem.ItemIsSelectable) self.init() @@ -363,13 +353,13 @@ def _set_tooltip(self): for effect in self.item: name = effect.name if effect.name else "" effect_name = effect.effect_name if effect.effect_name else "" - tool_tips.append("{} {}".format(name, effect_name)) + tool_tips.append(f"{name} {effect_name}") self.setToolTip("\n".join(tool_tips)) def paint(self, *args, **kwargs): new_args = [args[0], QtWidgets.QStyleOptionGraphicsItem()] + list(args[2:]) - super(EffectItem, self).paint(*new_args, **kwargs) + super().paint(*new_args, **kwargs) def itemChange(self, change, value): if change == QtWidgets.QGraphicsItem.ItemSelectedHasChanged: @@ -383,14 +373,14 @@ def itemChange(self, change, value): self.zValue() + 1 if self.isSelected() else self.zValue() - 1 ) - return super(EffectItem, self).itemChange(change, value) + return super().itemChange(change, value) class TransitionItem(BaseItem): def __init__(self, item, timeline_range, rect, *args, **kwargs): rect.setHeight(TRANSITION_HEIGHT) - super(TransitionItem, self).__init__( + super().__init__( item, timeline_range, rect, @@ -428,7 +418,7 @@ def _set_labels(self): class ClipItem(BaseItem): def __init__(self, *args, **kwargs): - super(ClipItem, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) self.setBrush(QtGui.QBrush(QtGui.QColor(168, 197, 255, 255) if self.item.enabled else QtGui.QColor(100, 100, 100, 255))) self.source_name_label.setText(self.item.name) @@ -437,7 +427,7 @@ def __init__(self, *args, **kwargs): class NestedItem(BaseItem): def __init__(self, *args, **kwargs): - super(NestedItem, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) self.setBrush( QtGui.QBrush(QtGui.QColor(255, 113, 91, 255)) ) @@ -445,11 +435,11 @@ def __init__(self, *args, **kwargs): self.source_name_label.setText(self.item.name) def mouseDoubleClickEvent(self, event): - super(NestedItem, self).mouseDoubleClickEvent(event) + super().mouseDoubleClickEvent(event) self.scene().views()[0].open_stack.emit(self.item) def keyPressEvent(self, key_event): - super(NestedItem, self).keyPressEvent(key_event) + super().keyPressEvent(key_event) key = key_event.key() if key == QtCore.Qt.Key_Return: @@ -459,7 +449,7 @@ def keyPressEvent(self, key_event): class Track(QtWidgets.QGraphicsRectItem): def __init__(self, track, *args, **kwargs): - super(Track, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) self.track = track self.widget_items = [] self.track_name_item = None @@ -504,7 +494,7 @@ def _populate(self): elif isinstance(item, otio.schema.Transition): new_item = TransitionItem(item, timeline_range, rect) else: - print("Warning: could not add item {} to UI.".format(item)) + print(f"Warning: could not add item {item} to UI.") continue new_item.setParentItem(self) @@ -529,17 +519,19 @@ def __init__(self, marker, *args, **kwargs): poly.append(QtCore.QPointF(0, MARKER_SIZE)) poly.append(QtCore.QPointF(-0.5 * MARKER_SIZE, 0.5 * MARKER_SIZE)) poly.append(QtCore.QPointF(-0.5 * MARKER_SIZE, -0.5 * MARKER_SIZE)) - super(Marker, self).__init__(poly, *args, **kwargs) + super().__init__(poly, *args, **kwargs) self.setFlags(QtWidgets.QGraphicsItem.ItemIsSelectable) + color = MARKER_COLORS.get(marker.color, (121, 212, 177, 255)) + self.setBrush( - QtGui.QBrush(QtGui.QColor(121, 212, 177, 255)) + QtGui.QBrush(QtGui.QColor(*color)) ) def paint(self, *args, **kwargs): new_args = [args[0], QtWidgets.QStyleOptionGraphicsItem()] + list(args[2:]) - super(Marker, self).paint(*new_args, **kwargs) + super().paint(*new_args, **kwargs) def itemChange(self, change, value): if change == QtWidgets.QGraphicsItem.ItemSelectedHasChanged: @@ -547,7 +539,7 @@ def itemChange(self, change, value): QtGui.QColor(0, 255, 0, 255) if self.isSelected() else QtGui.QColor(0, 0, 0, 255) ) - return super(Marker, self).itemChange(change, value) + return super().itemChange(change, value) def counteract_zoom(self, zoom_level=1.0): self.setTransform(QtGui.QTransform.fromScale(zoom_level, 1.0)) @@ -556,7 +548,7 @@ def counteract_zoom(self, zoom_level=1.0): class TimeSlider(QtWidgets.QGraphicsRectItem): def __init__(self, *args, **kwargs): - super(TimeSlider, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) self.setBrush(QtGui.QBrush(QtGui.QColor(64, 78, 87, 255))) pen = QtGui.QPen() pen.setWidth(0) @@ -571,7 +563,7 @@ def mousePressEvent(self, mouse_event): MARKER_SIZE)) self._ruler.update_frame() - super(TimeSlider, self).mousePressEvent(mouse_event) + super().mousePressEvent(mouse_event) def add_ruler(self, ruler): self._ruler = ruler diff --git a/src/py-opentimelineio/bindings-common/casters.h b/src/py-opentimelineio/bindings-common/casters.h new file mode 100644 index 000000000..5e6cf8f8b --- /dev/null +++ b/src/py-opentimelineio/bindings-common/casters.h @@ -0,0 +1,18 @@ +#include +#include +#include "nonstd/optional.hpp" + +using nonstd::optional; +using nonstd::nullopt_t; + +namespace pybind11 { namespace detail { + // Caster for converting to/from nonstd::optional. + // Pybind11 supports optional types when C++17 is used. + // So for now we have to create the casters manually. + // See https://pybind11.readthedocs.io/en/stable/advanced/cast/stl.html#c-17-library-containers + template struct type_caster> + : public optional_caster> {}; + + template<> struct type_caster + : public void_caster {}; +}} diff --git a/src/py-opentimelineio/opentime-bindings/opentime_bindings.cpp b/src/py-opentimelineio/opentime-bindings/opentime_bindings.cpp index c815438d3..55cf2a586 100644 --- a/src/py-opentimelineio/opentime-bindings/opentime_bindings.cpp +++ b/src/py-opentimelineio/opentime-bindings/opentime_bindings.cpp @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Contributors to the OpenTimelineIO project + #include #include "opentime_bindings.h" diff --git a/src/py-opentimelineio/opentime-bindings/opentime_bindings.h b/src/py-opentimelineio/opentime-bindings/opentime_bindings.h index 1664561cb..eb62dcd9a 100644 --- a/src/py-opentimelineio/opentime-bindings/opentime_bindings.h +++ b/src/py-opentimelineio/opentime-bindings/opentime_bindings.h @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Contributors to the OpenTimelineIO project + #ifndef OTIO_OPENTIME_BINDINGS_H #define OTIO_OPENTIME_BINDINGS_H diff --git a/src/py-opentimelineio/opentime-bindings/opentime_rationalTime.cpp b/src/py-opentimelineio/opentime-bindings/opentime_rationalTime.cpp index 7c8d94bda..776467c1a 100644 --- a/src/py-opentimelineio/opentime-bindings/opentime_rationalTime.cpp +++ b/src/py-opentimelineio/opentime-bindings/opentime_rationalTime.cpp @@ -1,8 +1,13 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Contributors to the OpenTimelineIO project + #include #include #include "opentime/rationalTime.h" #include "opentimelineio/stringUtils.h" +#include "opentimelineio/optional.h" +#include "py-opentimelineio/bindings-common/casters.h" namespace py = pybind11; using namespace pybind11::literals; @@ -25,11 +30,10 @@ struct ErrorStatusConverter { ErrorStatus error_status; }; - -IsDropFrameRate df_enum_converter(py::object& df) { - if (df.is(py::none())) { +IsDropFrameRate df_enum_converter(optional& df) { + if (!df.has_value()) { return IsDropFrameRate::InferFromRate; - } else if (py::cast(py::bool_(df))) { + } else if (df.value()) { return IsDropFrameRate::ForceYes; } else { return IsDropFrameRate::ForceNo; @@ -57,9 +61,15 @@ RationalTime _type_checked(py::object const& rhs, char const* op) { } void opentime_rationalTime_bindings(py::module m) { - py::class_(m, "RationalTime") + py::class_(m, "RationalTime", R"docstring( +The RationalTime class represents a measure of time of :math:`rt.value/rt.rate` seconds. +It can be rescaled into another :class:`~RationalTime`'s rate. +)docstring") .def(py::init(), "value"_a = 0, "rate"_a = 1) - .def("is_invalid_time", &RationalTime::is_invalid_time) + .def("is_invalid_time", &RationalTime::is_invalid_time, R"docstring( +Returns true if the time is invalid. The time is considered invalid if the value or the rate are a NaN value +or if the rate is less than or equal to zero. +)docstring") .def_property_readonly("value", &RationalTime::value) .def_property_readonly("rate", &RationalTime::rate) .def("rescaled_to", (RationalTime (RationalTime::*)(double) const) &RationalTime::rescaled_to, @@ -67,9 +77,9 @@ void opentime_rationalTime_bindings(py::module m) { .def("rescaled_to", (RationalTime (RationalTime::*)(RationalTime) const) &RationalTime::rescaled_to, "other"_a, R"docstring(Returns the time for time converted to new_rate.)docstring") .def("value_rescaled_to", (double (RationalTime::*)(double) const) &RationalTime::value_rescaled_to, - "new_rate"_a, R"docstring(Returns the time value for self converted to new_rate.)docstring") + "new_rate"_a, R"docstring(Returns the time value for "self" converted to new_rate.)docstring") .def("value_rescaled_to", (double (RationalTime::*)(RationalTime) const) &RationalTime::value_rescaled_to, - "other"_a, R"docstring(Returns the time value for self converted to new_rate.)docstring") + "other"_a) .def("almost_equal", &RationalTime::almost_equal, "other"_a, "delta"_a = 0) .def("__copy__", [](RationalTime rt) { return rt; @@ -78,25 +88,33 @@ void opentime_rationalTime_bindings(py::module m) { return rt; }, "copier"_a = py::none()) .def_static("duration_from_start_end_time", &RationalTime::duration_from_start_end_time, - "start_time"_a, "end_time_exclusive"_a) + "start_time"_a, "end_time_exclusive"_a, R"docstring( +Compute the duration of samples from first to last (excluding last). This is not the same as distance. + +For example, the duration of a clip from frame 10 to frame 15 is 5 frames. Result will be in the rate of start_time. +)docstring") .def_static("duration_from_start_end_time_inclusive", &RationalTime::duration_from_start_end_time_inclusive, - "start_time"_a, "end_time_inclusive"_a) - .def_static("is_valid_timecode_rate", &RationalTime::is_valid_timecode_rate, "rate"_a) + "start_time"_a, "end_time_inclusive"_a, R"docstring( +Compute the duration of samples from first to last (including last). This is not the same as distance. + +For example, the duration of a clip from frame 10 to frame 15 is 6 frames. Result will be in the rate of start_time. +)docstring") + .def_static("is_valid_timecode_rate", &RationalTime::is_valid_timecode_rate, "rate"_a, "Returns true if the rate is valid for use with timecode.") .def_static("nearest_valid_timecode_rate", &RationalTime::nearest_valid_timecode_rate, "rate"_a, - R"docstring(Returns the first valid timecode rate that has the least difference from the given value.)docstring") - .def_static("from_frames", &RationalTime::from_frames, "frame"_a, "rate"_a) + "Returns the first valid timecode rate that has the least difference from the given value.") + .def_static("from_frames", &RationalTime::from_frames, "frame"_a, "rate"_a, "Turn a frame number and rate into a :class:`~RationalTime` object.") .def_static("from_seconds", static_cast (&RationalTime::from_seconds), "seconds"_a, "rate"_a) .def_static("from_seconds", static_cast (&RationalTime::from_seconds), "seconds"_a) - .def("to_frames", (int (RationalTime::*)() const) &RationalTime::to_frames) - .def("to_frames", (int (RationalTime::*)(double) const) &RationalTime::to_frames, "rate"_a) + .def("to_frames", (int (RationalTime::*)() const) &RationalTime::to_frames, "Returns the frame number based on the current rate.") + .def("to_frames", (int (RationalTime::*)(double) const) &RationalTime::to_frames, "rate"_a, "Returns the frame number based on the given rate.") .def("to_seconds", &RationalTime::to_seconds) - .def("to_timecode", [](RationalTime rt, double rate, py::object drop_frame) { + .def("to_timecode", [](RationalTime rt, double rate, optional drop_frame) { return rt.to_timecode( rate, df_enum_converter(drop_frame), ErrorStatusConverter() ); - }, "rate"_a, "drop_frame"_a) + }, "rate"_a, "drop_frame"_a, "Convert to timecode (``HH:MM:SS;FRAME``)") .def("to_timecode", [](RationalTime rt, double rate) { return rt.to_timecode( rate, @@ -113,10 +131,10 @@ void opentime_rationalTime_bindings(py::module m) { .def("to_time_string", &RationalTime::to_time_string) .def_static("from_timecode", [](std::string s, double rate) { return RationalTime::from_timecode(s, rate, ErrorStatusConverter()); - }, "timecode"_a, "rate"_a) + }, "timecode"_a, "rate"_a, "Convert a timecode string (``HH:MM:SS;FRAME``) into a :class:`~RationalTime`.") .def_static("from_time_string", [](std::string s, double rate) { return RationalTime::from_time_string(s, rate, ErrorStatusConverter()); - }, "time_string"_a, "rate"_a) + }, "time_string"_a, "rate"_a, "Convert a time with microseconds string (``HH:MM:ss`` where ``ss`` is an integer or a decimal number) into a :class:`~RationalTime`.") .def("__str__", &opentime_python_str) .def("__repr__", &opentime_python_repr) .def(- py::self) diff --git a/src/py-opentimelineio/opentime-bindings/opentime_timeRange.cpp b/src/py-opentimelineio/opentime-bindings/opentime_timeRange.cpp index a054c2686..d010cf880 100644 --- a/src/py-opentimelineio/opentime-bindings/opentime_timeRange.cpp +++ b/src/py-opentimelineio/opentime-bindings/opentime_timeRange.cpp @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Contributors to the OpenTimelineIO project + #include #include @@ -11,7 +14,11 @@ using namespace opentime; void opentime_timeRange_bindings(py::module m) { - py::class_(m, "TimeRange") + py::class_(m, "TimeRange", R"docstring( +The TimeRange class represents a range in time. It encodes the start time and the duration, +meaning that :meth:`end_time_inclusive` (last portion of a sample in the time range) and +:meth:`end_time_exclusive` can be computed. +)docstring") // matches the python constructor behavior .def(py::init( [](RationalTime* start_time, RationalTime* duration) { @@ -38,12 +45,12 @@ void opentime_timeRange_bindings(py::module m) { .def_property_readonly("start_time", &TimeRange::start_time) .def_property_readonly("duration", &TimeRange::duration) .def("end_time_inclusive", &TimeRange::end_time_inclusive, R"docstring( -The time of the last sample containing data in the TimeRange. +The time of the last sample containing data in the time range. -If the TimeRange starts at (0,24) with duration (10,24), this will be -(9,24) +If the time range starts at (0, 24) with duration (10, 24), this will be +(9, 24) -If the TimeRange starts at (0,24) with duration (10.5, 24): +If the time range starts at (0, 24) with duration (10.5, 24): (10, 24) In other words, the last frame with data, even if the last frame is fractional. @@ -51,30 +58,31 @@ In other words, the last frame with data, even if the last frame is fractional. .def("end_time_exclusive", &TimeRange::end_time_exclusive, R"docstring( Time of the first sample outside the time range. -If Start frame is 10 and duration is 5, then end_time_exclusive is 15, +If start frame is 10 and duration is 5, then end_time_exclusive is 15, because the last time with data in this range is 14. -If Start frame is 10 and duration is 5.5, then end_time_exclusive is +If start frame is 10 and duration is 5.5, then end_time_exclusive is 15.5, because the last time with data in this range is 15. )docstring") .def("duration_extended_by", &TimeRange::duration_extended_by, "other"_a) - .def("extended_by", &TimeRange::extended_by, "other"_a, R"docstring(Construct a new TimeRange that is this one extended by other.)docstring") + .def("extended_by", &TimeRange::extended_by, "other"_a, "Construct a new :class:`~TimeRange` that is this one extended by other.") .def("clamped", (RationalTime (TimeRange::*)(RationalTime) const) &TimeRange::clamped, "other"_a, R"docstring( -Clamp 'other' (RationalTime) according to -self.start_time/end_time_exclusive and bound arguments. +Clamp 'other' (:class:`~RationalTime`) according to +:attr:`start_time`/:attr:`end_time_exclusive` and bound arguments. )docstring") .def("clamped", (TimeRange (TimeRange::*)(TimeRange) const) &TimeRange::clamped, "other"_a, R"docstring( -Clamp 'other' (TimeRange) according to -self.start_time/end_time_exclusive and bound arguments. +Clamp 'other' (:class:`~TimeRange`) according to +:attr:`start_time`/:attr:`end_time_exclusive` and bound arguments. )docstring") .def("contains", (bool (TimeRange::*)(RationalTime) const) &TimeRange::contains, "other"_a, R"docstring( The start of `this` precedes `other`. `other` precedes the end of `this`. :: - other - ↓ - * - [ this ] + + other + ↓ + * + [ this ] )docstring") .def("contains", (bool (TimeRange::*)(TimeRange, double) const) &TimeRange::contains, "other"_a, "epsilon_s"_a=opentime::DEFAULT_EPSILON_s, R"docstring( @@ -82,86 +90,106 @@ The start of `this` precedes start of `other`. The end of `this` antecedes end of `other`. :: - [ other ] - [ this ] + [ other ] + [ this ] The converse would be ``other.contains(this)`` )docstring") .def("overlaps", (bool (TimeRange::*)(RationalTime) const) &TimeRange::overlaps, "other"_a, R"docstring( `this` contains `other`. :: - other - ↓ - * - [ this ] + + other + ↓ + * + [ this ] + )docstring") .def("overlaps", (bool (TimeRange::*)(TimeRange, double) const) &TimeRange::overlaps, "other"_a, "epsilon_s"_a=opentime::DEFAULT_EPSILON_s, R"docstring( The start of `this` strictly precedes end of `other` by a value >= `epsilon_s`. The end of `this` strictly antecedes start of `other` by a value >= `epsilon_s`. :: - [ this ] - [ other ] + + [ this ] + [ other ] + The converse would be ``other.overlaps(this)`` )docstring") .def("before", (bool (TimeRange::*)(RationalTime, double ) const) &TimeRange::before, "other"_a, "epsilon_s"_a=opentime::DEFAULT_EPSILON_s, R"docstring( The end of `this` strictly precedes `other` by a value >= `epsilon_s`. :: - other - ↓ - [ this ] * + + other + ↓ + [ this ] * + )docstring") .def("before", (bool (TimeRange::*)(TimeRange, double) const) &TimeRange::before, "other"_a, "epsilon_s"_a=opentime::DEFAULT_EPSILON_s, R"docstring( The end of `this` strictly equals the start of `other` and the start of `this` strictly equals the end of `other`. :: - [this][other] + + [this][other] + The converse would be ``other.meets(this)`` )docstring") .def("meets", (bool (TimeRange::*)(TimeRange, double) const) &TimeRange::meets, "other"_a, "epsilon_s"_a=opentime::DEFAULT_EPSILON_s, R"docstring( The end of `this` strictly equals the start of `other` and the start of `this` strictly equals the end of `other`. :: - [this][other] + + [this][other] + The converse would be ``other.meets(this)`` )docstring") .def("begins", (bool (TimeRange::*)(RationalTime, double) const) &TimeRange::begins, "other"_a, "epsilon_s"_a=opentime::DEFAULT_EPSILON_s, R"docstring( The start of `this` strictly equals `other`. :: - other - ↓ - * - [ this ] + + other + ↓ + * + [ this ] + )docstring") .def("begins", (bool (TimeRange::*)(TimeRange, double) const) &TimeRange::begins, "other"_a, "epsilon_s"_a=opentime::DEFAULT_EPSILON_s, R"docstring( The start of `this` strictly equals the start of `other`. The end of `this` strictly precedes the end of `other` by a value >= `epsilon_s`. :: - [ this ] - [ other ] + + [ this ] + [ other ] + The converse would be ``other.begins(this)`` )docstring") .def("finishes", (bool (TimeRange::*)(RationalTime, double) const) &TimeRange::finishes, "other"_a, "epsilon_s"_a=opentime::DEFAULT_EPSILON_s, R"docstring( The end of `this` strictly equals `other`. :: - other - ↓ - * - [ this ] + + other + ↓ + * + [ this ] + )docstring") .def("finishes", (bool (TimeRange::*)(TimeRange, double) const) &TimeRange::finishes, "other"_a, "epsilon_s"_a=opentime::DEFAULT_EPSILON_s, R"docstring( The start of `this` strictly antecedes the start of `other` by a value >= `epsilon_s`. The end of `this` strictly equals the end of `other`. :: - [ this ] - [ other ] + + [ this ] + [ other ] + The converse would be ``other.finishes(this)`` )docstring") .def("intersects", (bool (TimeRange::*)(TimeRange, double) const) &TimeRange::intersects, "other"_a, "epsilon_s"_a=opentime::DEFAULT_EPSILON_s, R"docstring( The start of `this` precedes or equals the end of `other` by a value >= `epsilon_s`. The end of `this` antecedes or equals the start of `other` by a value >= `epsilon_s`. :: - [ this ] OR [ other ] - [ other ] [ this ] + + [ this ] OR [ other ] + [ other ] [ this ] + The converse would be ``other.finishes(this)`` )docstring") .def("__copy__", [](TimeRange tr) { @@ -171,9 +199,17 @@ The converse would be ``other.finishes(this)`` return tr; }) .def_static("range_from_start_end_time", &TimeRange::range_from_start_end_time, - "start_time"_a, "end_time_exclusive"_a) + "start_time"_a, "end_time_exclusive"_a, R"docstring( +Creates a :class:`~TimeRange` from start and end :class:`~RationalTime`\s (exclusive). + +For example, if start_time is 1 and end_time is 10, the returned will have a duration of 9. +)docstring") .def_static("range_from_start_end_time_inclusive", &TimeRange::range_from_start_end_time_inclusive, - "start_time"_a, "end_time_inclusive"_a) + "start_time"_a, "end_time_inclusive"_a, R"docstring( +Creates a :class:`~TimeRange` from start and end :class:`~RationalTime`\s (inclusive). + +For example, if start_time is 1 and end_time is 10, the returned will have a duration of 10. +)docstring") .def(py::self == py::self) .def(py::self != py::self) .def("__str__", [](TimeRange tr) { diff --git a/src/py-opentimelineio/opentime-bindings/opentime_timeTransform.cpp b/src/py-opentimelineio/opentime-bindings/opentime_timeTransform.cpp index a4d6c7d48..26a05b245 100644 --- a/src/py-opentimelineio/opentime-bindings/opentime_timeTransform.cpp +++ b/src/py-opentimelineio/opentime-bindings/opentime_timeTransform.cpp @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Contributors to the OpenTimelineIO project + #include #include @@ -11,7 +14,7 @@ using namespace opentime; void opentime_timeTransform_bindings(py::module m) { - py::class_(m, "TimeTransform") + py::class_(m, "TimeTransform", R"docstring(1D transform for :class:`~RationalTime`. Has offset and scale.)docstring") .def(py::init(), "offset"_a = RationalTime(), "scale"_a = 1, "rate"_a = -1) .def_property_readonly("offset", &TimeTransform::offset) diff --git a/src/py-opentimelineio/opentimelineio-bindings/otio_anyDictionary.cpp b/src/py-opentimelineio/opentimelineio-bindings/otio_anyDictionary.cpp index afcfaf65e..385fd70db 100644 --- a/src/py-opentimelineio/opentimelineio-bindings/otio_anyDictionary.cpp +++ b/src/py-opentimelineio/opentimelineio-bindings/otio_anyDictionary.cpp @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Contributors to the OpenTimelineIO project + #include #include @@ -16,11 +19,7 @@ using namespace pybind11::literals; void otio_any_dictionary_bindings(py::module m) { py::class_(m, "AnyDictionaryIterator") .def("__iter__", &AnyDictionaryProxy::Iterator::iter) - #if PY_MAJOR_VERSION >= 3 .def("__next__", &AnyDictionaryProxy::Iterator::next); - #else - .def("next", &AnyDictionaryProxy::Iterator::next); - #endif py::class_(m, "AnyDictionary") .def(py::init<>()) diff --git a/src/py-opentimelineio/opentimelineio-bindings/otio_anyDictionary.h b/src/py-opentimelineio/opentimelineio-bindings/otio_anyDictionary.h index 9deeb6f10..c49ebe887 100644 --- a/src/py-opentimelineio/opentimelineio-bindings/otio_anyDictionary.h +++ b/src/py-opentimelineio/opentimelineio-bindings/otio_anyDictionary.h @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Contributors to the OpenTimelineIO project + #pragma once #include @@ -14,7 +17,7 @@ struct AnyDictionaryProxy : public AnyDictionary::MutationStamp { using MutationStamp = AnyDictionary::MutationStamp; static void throw_dictionary_was_deleted() { - throw py::value_error("underlying C++ AnyDictionary has been destroyed"); + throw py::value_error("Underlying C++ AnyDictionary has been destroyed"); } struct Iterator { diff --git a/src/py-opentimelineio/opentimelineio-bindings/otio_anyVector.cpp b/src/py-opentimelineio/opentimelineio-bindings/otio_anyVector.cpp index cc9158d5e..4d974383b 100644 --- a/src/py-opentimelineio/opentimelineio-bindings/otio_anyVector.cpp +++ b/src/py-opentimelineio/opentimelineio-bindings/otio_anyVector.cpp @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Contributors to the OpenTimelineIO project + #include #include @@ -12,11 +15,7 @@ using namespace pybind11::literals; void otio_any_vector_bindings(py::module m) { py::class_(m, "AnyVectorIterator") .def("__iter__", &AnyVectorProxy::Iterator::iter) - #if PY_MAJOR_VERSION >= 3 .def("__next__", &AnyVectorProxy::Iterator::next); - #else - .def("next", &AnyVectorProxy::Iterator::next); - #endif py::class_(m, "AnyVector") .def(py::init<>()) diff --git a/src/py-opentimelineio/opentimelineio-bindings/otio_anyVector.h b/src/py-opentimelineio/opentimelineio-bindings/otio_anyVector.h index 608396d70..ed43e5dec 100644 --- a/src/py-opentimelineio/opentimelineio-bindings/otio_anyVector.h +++ b/src/py-opentimelineio/opentimelineio-bindings/otio_anyVector.h @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Contributors to the OpenTimelineIO project + #pragma once #include #include "opentimelineio/anyVector.h" @@ -11,7 +14,7 @@ struct AnyVectorProxy : public AnyVector::MutationStamp { using MutationStamp = AnyVector::MutationStamp; static void throw_array_was_deleted() { - throw py::value_error("underlying C++ AnyVector object has been destroyed"); + throw py::value_error("Underlying C++ AnyVector object has been destroyed"); } struct Iterator { @@ -43,7 +46,7 @@ struct AnyVectorProxy : public AnyVector::MutationStamp { AnyVector& v = fetch_any_vector(); index = adjusted_vector_index(index, v); if (index < 0 || index >= int(v.size())) { - throw py::index_error(); + throw py::index_error("list index out of range"); } return any_to_py(v[index]); } @@ -52,7 +55,7 @@ struct AnyVectorProxy : public AnyVector::MutationStamp { AnyVector& v = fetch_any_vector(); index = adjusted_vector_index(index, v); if (index < 0 || index >= int(v.size())) { - throw py::index_error(); + throw py::index_error("list assignment index out of range"); } std::swap(v[index], pyAny->a); } @@ -72,7 +75,7 @@ struct AnyVectorProxy : public AnyVector::MutationStamp { void del_item(int index) { AnyVector& v = fetch_any_vector(); if (v.empty()) { - throw py::index_error(); + throw py::index_error("list index out of range"); } index = adjusted_vector_index(index, v); diff --git a/src/py-opentimelineio/opentimelineio-bindings/otio_bindings.cpp b/src/py-opentimelineio/opentimelineio-bindings/otio_bindings.cpp index 5c0e5bbbb..74572125c 100644 --- a/src/py-opentimelineio/opentimelineio-bindings/otio_bindings.cpp +++ b/src/py-opentimelineio/opentimelineio-bindings/otio_bindings.cpp @@ -1,4 +1,8 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Contributors to the OpenTimelineIO project + #include +#include #include "otio_anyDictionary.h" #include "otio_anyVector.h" #include "otio_bindings.h" @@ -13,6 +17,9 @@ namespace py = pybind11; using namespace pybind11::literals; +// temporarily disabling this feature while I chew on it +const static bool EXCEPTION_ON_DOUBLE_REGISTER = false; + static void register_python_type(py::object class_object, std::string schema_name, int schema_version) { @@ -33,23 +40,116 @@ static void register_python_type(py::object class_object, return r.take_value(); }; - TypeRegistry::instance().register_type(schema_name, schema_version, - nullptr, create, schema_name); + + // @TODO: further discussion required about preventing double registering +#if 0 + if ( + !TypeRegistry::instance().register_type( + schema_name, + schema_version, + nullptr, + create, + schema_name + ) + && EXCEPTION_ON_DOUBLE_REGISTER + ) { + auto err = ErrorStatusHandler(); + err.error_status = ErrorStatus( + ErrorStatus::INTERNAL_ERROR, + "Schema: " + schema_name + " has already been registered." + ); + } +#else + TypeRegistry::instance().register_type( + schema_name, + schema_version, + nullptr, + create, + schema_name + ); +#endif } static bool register_upgrade_function(std::string const& schema_name, int version_to_upgrade_to, - py::object const& upgrade_function_obj) { + std::function const& upgrade_function_obj) { std::function upgrade_function = [upgrade_function_obj](AnyDictionary* d) { py::gil_scoped_acquire acquire; auto ptr = d->get_or_create_mutation_stamp(); - py::object dobj = py::cast((AnyDictionaryProxy*)ptr); - upgrade_function_obj(dobj); + upgrade_function_obj((AnyDictionaryProxy*)ptr); }; - return TypeRegistry::instance().register_upgrade_function(schema_name, version_to_upgrade_to, - upgrade_function); + // further discussion required about preventing double registering +#if 0 + if ( + !TypeRegistry::instance().register_upgrade_function( + schema_name, + version_to_upgrade_to, + upgrade_function + ) //&& EXCEPTION_ON_DOUBLE_REGISTER + ) + { + auto err = ErrorStatusHandler(); + err.error_status = ErrorStatus( + ErrorStatus::INTERNAL_ERROR, + "Upgrade function already exists for " + schema_name + ); + return false; + } + + return true; +#else + return TypeRegistry::instance().register_upgrade_function( + schema_name, + version_to_upgrade_to, + upgrade_function + ); +#endif +} + +static bool +register_downgrade_function( + std::string const& schema_name, + int version_to_downgrade_from, + std::function const& downgrade_function_obj) +{ + std::function downgrade_function = ( + [downgrade_function_obj](AnyDictionary* d) + { + py::gil_scoped_acquire acquire; + + auto ptr = d->get_or_create_mutation_stamp(); + downgrade_function_obj((AnyDictionaryProxy*)ptr); + } + ); + + // further discussion required about preventing double registering +#if 0 + if ( + !TypeRegistry::instance().register_downgrade_function( + schema_name, + version_to_downgrade_from, + downgrade_function + ) //&& EXCEPTION_ON_DOUBLE_REGISTER + ) + { + auto err = ErrorStatusHandler(); + err.error_status = ErrorStatus( + ErrorStatus::INTERNAL_ERROR, + "Downgrade function already exists for " + schema_name + ); + return false; + } + return true; +#else + return TypeRegistry::instance().register_downgrade_function( + schema_name, + version_to_downgrade_from, + downgrade_function + ) ; +#endif + } static void set_type_record(SerializableObject* so, std::string schema_name) { @@ -65,7 +165,13 @@ static SerializableObject* instance_from_schema(std::string schema_name, } PYBIND11_MODULE(_otio, m) { + // Import _opentime before actually creating the bindings + // for _otio. This allows the import of _otio without + // manually importing _opentime before. For example: python -c 'import opentimelineio._otio' + py::module_::import("opentimelineio._opentime"); + m.doc() = "Bindings to C++ OTIO implementation"; + otio_exception_bindings(m); otio_any_dictionary_bindings(m); otio_any_vector_bindings(m); @@ -73,26 +179,75 @@ PYBIND11_MODULE(_otio, m) { otio_serializable_object_bindings(m); otio_tests_bindings(m); - m.def("_serialize_json_to_string", - [](PyAny* pyAny, int indent) { - return serialize_json_to_string(pyAny->a, ErrorStatusHandler(), indent); - }, "value"_a, "indent"_a) + m.def( + "_serialize_json_to_string", + []( + PyAny* pyAny, + const schema_version_map& schema_version_targets, + int indent + ) + { + auto result = serialize_json_to_string( + pyAny->a, + &schema_version_targets, + ErrorStatusHandler(), + indent + ); + + return result; + }, + "value"_a, + "schema_version_targets"_a, + "indent"_a + ) .def("_serialize_json_to_file", - [](PyAny* pyAny, std::string filename, int indent) { - return serialize_json_to_file(pyAny->a, filename, ErrorStatusHandler(), indent); - }, "value"_a, "filename"_a, "indent"_a) + []( + PyAny* pyAny, + std::string filename, + const schema_version_map& schema_version_targets, + int indent + ) { + return serialize_json_to_file( + pyAny->a, + filename, + &schema_version_targets, + ErrorStatusHandler(), + indent + ); + }, + "value"_a, + "filename"_a, + "schema_version_targets"_a, + "indent"_a) .def("deserialize_json_from_string", [](std::string input) { any result; deserialize_json_from_string(input, &result, ErrorStatusHandler()); return any_to_py(result, true /*top_level*/); - }, "input"_a) + }, "input"_a, + R"docstring(Deserialize json string to in-memory objects. + +:param str input: json string to deserialize + +:returns: root object in the string (usually a Timeline or SerializableCollection) +:rtype: SerializableObject + +)docstring") .def("deserialize_json_from_file", [](std::string filename) { any result; deserialize_json_from_file(filename, &result, ErrorStatusHandler()); return any_to_py(result, true /*top_level*/); - }, "filename"_a); + }, + "filename"_a, + R"docstring(Deserialize json file to in-memory objects. + +:param str filename: path to json file to read + +:returns: root object in the file (usually a Timeline or SerializableCollection) +:rtype: SerializableObject + +)docstring"); py::class_(m, "PyAny") // explicitly map python bool, int and double classes so that they @@ -125,16 +280,45 @@ PYBIND11_MODULE(_otio, m) { m.def("install_external_keepalive_monitor", &install_external_keepalive_monitor, "so"_a, "apply_now"_a); m.def("instance_from_schema", &instance_from_schema, - "schema_name"_a, "schema_version"_a, "data"_a); + "schema_name"_a, "schema_version"_a, "data"_a, R"docstring( +Return an instance of the schema from data in the data_dict. + +:raises UnsupportedSchemaError: when the requested schema version is greater than the registered schema version. +)docstring"); + m.def("type_version_map", + []() { + schema_version_map tmp; + TypeRegistry::instance().type_version_map(tmp); + return tmp; + }, + R"docstring(Fetch the currently registered schemas and their versions. + +:returns: Map of all registered schema names to their current versions. +:rtype: dict[str, int])docstring" + ); m.def("register_upgrade_function", ®ister_upgrade_function, "schema_name"_a, "version_to_upgrade_to"_a, "upgrade_function"_a); + m.def("register_downgrade_function", ®ister_downgrade_function, + "schema_name"_a, + "version_to_downgrade_from"_a, + "downgrade_function"_a); + m.def( + "release_to_schema_version_map", + [](){ return label_to_schema_version_map(CORE_VERSION_MAP);}, + R"docstring(Fetch the compiled in CORE_VERSION_MAP. + +The CORE_VERSION_MAP maps OTIO release versions to maps of schema name to schema version and is autogenerated by the OpenTimelineIO build and release system. For example: `{"0.15.0": {"Clip": 2, ...}}` + +:returns: dictionary mapping core version label to schema_version_map +:rtype: dict[str, dict[str, int]])docstring" + ); m.def("flatten_stack", [](Stack* s) { return flatten_stack(s, ErrorStatusHandler()); }, "in_stack"_a); - m.def("flatten_stack", [](py::object tracks) { - return flatten_stack(py_to_vector(tracks), ErrorStatusHandler()); + m.def("flatten_stack", [](std::vector tracks) { + return flatten_stack(tracks, ErrorStatusHandler()); }, "tracks"_a); void _build_any_to_py_dispatch_table(); diff --git a/src/py-opentimelineio/opentimelineio-bindings/otio_bindings.h b/src/py-opentimelineio/opentimelineio-bindings/otio_bindings.h index 40f306a6e..dc5287076 100644 --- a/src/py-opentimelineio/opentimelineio-bindings/otio_bindings.h +++ b/src/py-opentimelineio/opentimelineio-bindings/otio_bindings.h @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Contributors to the OpenTimelineIO project + #pragma once #include diff --git a/src/py-opentimelineio/opentimelineio-bindings/otio_errorStatusHandler.cpp b/src/py-opentimelineio/opentimelineio-bindings/otio_errorStatusHandler.cpp index 4693052a2..506a5db68 100644 --- a/src/py-opentimelineio/opentimelineio-bindings/otio_errorStatusHandler.cpp +++ b/src/py-opentimelineio/opentimelineio-bindings/otio_errorStatusHandler.cpp @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Contributors to the OpenTimelineIO project + #include "otio_errorStatusHandler.h" #include "opentimelineio/stringUtils.h" #include "opentimelineio/serializableObject.h" @@ -47,9 +50,11 @@ ErrorStatusHandler::~ErrorStatusHandler() noexcept(false) { case ErrorStatus::JSON_PARSE_ERROR: throw py::value_error("JSON parse error while reading: " + details()); case ErrorStatus::FILE_OPEN_FAILED: - throw py::value_error("failed to open file for reading: " + details()); + PyErr_SetFromErrnoWithFilename(PyExc_OSError, details().c_str()); + throw py::error_already_set(); case ErrorStatus::FILE_WRITE_FAILED: - throw py::value_error("failed to open file for writing: " + details()); + PyErr_SetFromErrnoWithFilename(PyExc_OSError, details().c_str()); + throw py::error_already_set(); case ErrorStatus::SCHEMA_VERSION_UNSUPPORTED: throw _UnsupportedSchemaException(full_details()); case ErrorStatus::NOT_A_CHILD_OF: diff --git a/src/py-opentimelineio/opentimelineio-bindings/otio_errorStatusHandler.h b/src/py-opentimelineio/opentimelineio-bindings/otio_errorStatusHandler.h index 5a5ef51b6..882ad8bf0 100644 --- a/src/py-opentimelineio/opentimelineio-bindings/otio_errorStatusHandler.h +++ b/src/py-opentimelineio/opentimelineio-bindings/otio_errorStatusHandler.h @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Contributors to the OpenTimelineIO project + #pragma once #include diff --git a/src/py-opentimelineio/opentimelineio-bindings/otio_imath.cpp b/src/py-opentimelineio/opentimelineio-bindings/otio_imath.cpp index b2896b20c..540025a55 100644 --- a/src/py-opentimelineio/opentimelineio-bindings/otio_imath.cpp +++ b/src/py-opentimelineio/opentimelineio-bindings/otio_imath.cpp @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Contributors to the OpenTimelineIO project + #include #include @@ -15,108 +18,111 @@ CLASS _type_checked(py::object const& rhs, char const* op) { } catch (...) { std::string rhs_type = py::cast(rhs.get_type().attr("__name__")); - throw py::type_error(string_printf("unsupported operand type(s) for %s: " + throw py::type_error(string_printf("Unsupported operand type(s) for %s: " "%s and %s", typeid(CLASS).name(), op, rhs_type.c_str())); } } static void define_imath_2d(py::module m) { - py::class_(m, "V2d") + // Note that module_local is used to avoid issues when + // Imath classes are binded with Pybind11 more than once. + // Using module_local will avoid conflicts in such cases. + py::class_(m, "V2d", py::module_local()) .def(py::init<>()) .def(py::init()) .def(py::init()) - .def_readwrite("x", &Imath::V2d::x) - .def_readwrite("y", &Imath::V2d::y) - .def("__getitem__", [](Imath::V2d const &v, size_t i) { + .def_readwrite("x", &IMATH_NAMESPACE::V2d::x) + .def_readwrite("y", &IMATH_NAMESPACE::V2d::y) + .def("__getitem__", [](IMATH_NAMESPACE::V2d const &v, size_t i) { return v[i]; }) - .def("__eq__", [](Imath::V2d lhs, py::object const& rhs) { - return lhs == _type_checked(rhs, "=="); + .def("__eq__", [](IMATH_NAMESPACE::V2d lhs, py::object const& rhs) { + return lhs == _type_checked(rhs, "=="); }) - .def("__ne__", [](Imath::V2d lhs, py::object const& rhs) { - return lhs != _type_checked(rhs, "!="); + .def("__ne__", [](IMATH_NAMESPACE::V2d lhs, py::object const& rhs) { + return lhs != _type_checked(rhs, "!="); }) - .def("__xor__", [](Imath::V2d lhs, py::object const& rhs) { - return lhs ^ _type_checked(rhs, "^"); + .def("__xor__", [](IMATH_NAMESPACE::V2d lhs, py::object const& rhs) { + return lhs ^ _type_checked(rhs, "^"); }) - .def("__mod__", [](Imath::V2d lhs, py::object const& rhs) { - return lhs % _type_checked(rhs, "%"); + .def("__mod__", [](IMATH_NAMESPACE::V2d lhs, py::object const& rhs) { + return lhs % _type_checked(rhs, "%"); }) - .def("__iadd__", [](Imath::V2d lhs, Imath::V2d rhs) { + .def("__iadd__", [](IMATH_NAMESPACE::V2d lhs, IMATH_NAMESPACE::V2d rhs) { return lhs += rhs; }) - .def("__isub__", [](Imath::V2d lhs, Imath::V2d rhs) { + .def("__isub__", [](IMATH_NAMESPACE::V2d lhs, IMATH_NAMESPACE::V2d rhs) { return lhs -= rhs; }) - .def("__imul__", [](Imath::V2d lhs, Imath::V2d rhs) { + .def("__imul__", [](IMATH_NAMESPACE::V2d lhs, IMATH_NAMESPACE::V2d rhs) { return lhs *= rhs; }) - .def("__idiv__", [](Imath::V2d lhs, Imath::V2d rhs) { + .def("__idiv__", [](IMATH_NAMESPACE::V2d lhs, IMATH_NAMESPACE::V2d rhs) { return lhs /= rhs; }) .def(py::self - py::self) .def(py::self + py::self) .def(py::self * py::self) .def(py::self / py::self) - .def("equalWithAbsError", [](Imath::V2d* v, Imath::V2d const & v2, double e) { + .def("equalWithAbsError", [](IMATH_NAMESPACE::V2d* v, IMATH_NAMESPACE::V2d const & v2, double e) { return v->equalWithAbsError(v2, e); }) - .def("equalWithRelError", [](Imath::V2d* v, Imath::V2d const & v2, double e) { + .def("equalWithRelError", [](IMATH_NAMESPACE::V2d* v, IMATH_NAMESPACE::V2d const & v2, double e) { return v->equalWithRelError(v2, e); }) - .def("dot", [](Imath::V2d* v, Imath::V2d const & v2) { + .def("dot", [](IMATH_NAMESPACE::V2d* v, IMATH_NAMESPACE::V2d const & v2) { return v->dot(v2); }) - .def("cross", [](Imath::V2d* v, Imath::V2d const & v2) { + .def("cross", [](IMATH_NAMESPACE::V2d* v, IMATH_NAMESPACE::V2d const & v2) { return v->cross(v2); }) - .def("length", &Imath::V2d::length) - .def("length2", &Imath::V2d::length2) - .def("normalize", &Imath::V2d::normalize) - .def("normalizeExc", &Imath::V2d::normalizeExc) - .def("normalizeNonNull", &Imath::V2d::normalizeNonNull) - .def("normalized", &Imath::V2d::normalized) - .def("normalizedExc", &Imath::V2d::normalizedExc) - .def("normalizedNonNull", &Imath::V2d::normalizedNonNull) + .def("length", &IMATH_NAMESPACE::V2d::length) + .def("length2", &IMATH_NAMESPACE::V2d::length2) + .def("normalize", &IMATH_NAMESPACE::V2d::normalize) + .def("normalizeExc", &IMATH_NAMESPACE::V2d::normalizeExc) + .def("normalizeNonNull", &IMATH_NAMESPACE::V2d::normalizeNonNull) + .def("normalized", &IMATH_NAMESPACE::V2d::normalized) + .def("normalizedExc", &IMATH_NAMESPACE::V2d::normalizedExc) + .def("normalizedNonNull", &IMATH_NAMESPACE::V2d::normalizedNonNull) .def_static("baseTypeLowest", []() { - return Imath::V2d::baseTypeLowest(); + return IMATH_NAMESPACE::V2d::baseTypeLowest(); }) .def_static("baseTypeMax", []() { - return Imath::V2d::baseTypeMax(); + return IMATH_NAMESPACE::V2d::baseTypeMax(); }) .def_static("baseTypeSmallest", []() { - return Imath::V2d::baseTypeSmallest(); + return IMATH_NAMESPACE::V2d::baseTypeSmallest(); }) .def_static("baseTypeEpsilon", []() { - return Imath::V2d::baseTypeEpsilon(); + return IMATH_NAMESPACE::V2d::baseTypeEpsilon(); }) .def_static("dimensions", []() { - return Imath::V2d::dimensions(); + return IMATH_NAMESPACE::V2d::dimensions(); }); - py::class_(m, "Box2d") + py::class_(m, "Box2d", py::module_local()) .def(py::init<>()) - .def(py::init()) - .def(py::init()) - .def_readwrite("min", &Imath::Box2d::min) - .def_readwrite("max", &Imath::Box2d::max) - .def("__eq__", [](Imath::Box2d lhs, py::object const& rhs) { - return lhs == _type_checked(rhs, "=="); + .def(py::init()) + .def(py::init()) + .def_readwrite("min", &IMATH_NAMESPACE::Box2d::min) + .def_readwrite("max", &IMATH_NAMESPACE::Box2d::max) + .def("__eq__", [](IMATH_NAMESPACE::Box2d lhs, py::object const& rhs) { + return lhs == _type_checked(rhs, "=="); }) - .def("__ne__", [](Imath::Box2d lhs, py::object const& rhs) { - return lhs != _type_checked(rhs, "!="); + .def("__ne__", [](IMATH_NAMESPACE::Box2d lhs, py::object const& rhs) { + return lhs != _type_checked(rhs, "!="); }) - .def("center", &Imath::Box2d::center) - .def("extendBy", [](Imath::Box2d* box, Imath::V2d const& point ) { + .def("center", &IMATH_NAMESPACE::Box2d::center) + .def("extendBy", [](IMATH_NAMESPACE::Box2d* box, IMATH_NAMESPACE::V2d const& point ) { return box->extendBy(point); }) - .def("extendBy", [](Imath::Box2d* box, Imath::Box2d const& rhs ) { + .def("extendBy", [](IMATH_NAMESPACE::Box2d* box, IMATH_NAMESPACE::Box2d const& rhs ) { return box->extendBy(rhs); }) - .def("intersects", [](Imath::Box2d* box, Imath::V2d const& point ) { + .def("intersects", [](IMATH_NAMESPACE::Box2d* box, IMATH_NAMESPACE::V2d const& point ) { return box->intersects(point); }) - .def("intersects", [](Imath::Box2d* box, Imath::Box2d const& rhs ) { + .def("intersects", [](IMATH_NAMESPACE::Box2d* box, IMATH_NAMESPACE::Box2d const& rhs ) { return box->intersects(rhs); }); } @@ -124,4 +130,3 @@ static void define_imath_2d(py::module m) { void otio_imath_bindings(py::module m) { define_imath_2d(m); } - diff --git a/src/py-opentimelineio/opentimelineio-bindings/otio_serializableObjects.cpp b/src/py-opentimelineio/opentimelineio-bindings/otio_serializableObjects.cpp index f6ee75144..e3ac4b4e2 100644 --- a/src/py-opentimelineio/opentimelineio-bindings/otio_serializableObjects.cpp +++ b/src/py-opentimelineio/opentimelineio-bindings/otio_serializableObjects.cpp @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Contributors to the OpenTimelineIO project + #include #include #include @@ -46,20 +49,21 @@ using TrackVectorProxy = using SOWithMetadata = SerializableObjectWithMetadata; namespace { - const std::string string_or_none_converter(py::object& thing) { - if (thing.is(py::none())) { - return std::string(); - } - else { - return py::str(thing); + + template + std::vector vector_or_default(optional> item) { + if (item.has_value()) { + return item.value(); } + + return std::vector(); } template - bool children_if(T* t, py::object descended_from_type, optional const& search_range, bool shallow_search, std::vector& l) { + bool find_children(T* t, py::object descended_from_type, optional const& search_range, bool shallow_search, std::vector& l) { if (descended_from_type.is(py::type::handle_of())) { - for (const auto& child : t->template children_if(ErrorStatusHandler(), search_range, shallow_search)) { + for (const auto& child : t->template find_children(ErrorStatusHandler(), search_range, shallow_search)) { l.push_back(child.value); } return true; @@ -68,30 +72,30 @@ namespace { } template - std::vector children_if(T* t, py::object descended_from_type, optional const& search_range, bool shallow_search = false) { + std::vector find_children(T* t, py::object descended_from_type, optional const& search_range, bool shallow_search = false) { std::vector l; - if (children_if(t, descended_from_type, search_range, shallow_search, l)) ; - else if (children_if(t, descended_from_type, search_range, shallow_search, l)) ; - else if (children_if(t, descended_from_type, search_range, shallow_search, l)) ; - else if (children_if(t, descended_from_type, search_range, shallow_search, l)) ; - else if (children_if(t, descended_from_type, search_range, shallow_search, l)) ; - else if (children_if(t, descended_from_type, search_range, shallow_search, l)) ; - else if (children_if(t, descended_from_type, search_range, shallow_search, l)) ; - else if (children_if(t, descended_from_type, search_range, shallow_search, l)) ; + if (find_children(t, descended_from_type, search_range, shallow_search, l)) ; + else if (find_children(t, descended_from_type, search_range, shallow_search, l)) ; + else if (find_children(t, descended_from_type, search_range, shallow_search, l)) ; + else if (find_children(t, descended_from_type, search_range, shallow_search, l)) ; + else if (find_children(t, descended_from_type, search_range, shallow_search, l)) ; + else if (find_children(t, descended_from_type, search_range, shallow_search, l)) ; + else if (find_children(t, descended_from_type, search_range, shallow_search, l)) ; + else if (find_children(t, descended_from_type, search_range, shallow_search, l)) ; else { - for (const auto& child : t->template children_if(ErrorStatusHandler(), search_range, shallow_search)) { + for (const auto& child : t->template find_children(ErrorStatusHandler(), search_range, shallow_search)) { l.push_back(child.value); } } return l; } - + template - std::vector clip_if(T* t, optional const& search_range, bool shallow_search = false) { + std::vector find_clips(T* t, optional const& search_range, bool shallow_search = false) { std::vector l; - for (const auto& child : t->clip_if(ErrorStatusHandler(), search_range, shallow_search)) { - l.push_back(child.value); + for (const auto& clip : t->find_clips(ErrorStatusHandler(), search_range, shallow_search)) { + l.push_back(clip.value); } return l; } @@ -151,7 +155,7 @@ class ContainerIterator { }; static void define_bases1(py::module m) { - py::class_>(m, "SerializableObject", py::dynamic_attr()) + py::class_>(m, "SerializableObject", py::dynamic_attr(), "Superclass for all classes whose instances can be serialized.") .def(py::init<>()) .def_property_readonly("_dynamic_fields", [](SerializableObject* s) { auto ptr = s->dynamic_fields().get_or_create_mutation_stamp(); @@ -160,10 +164,10 @@ static void define_bases1(py::module m) { .def("clone", [](SerializableObject* so) { return so->clone(ErrorStatusHandler()); }) .def("to_json_string", [](SerializableObject* so, int indent) { - return so->to_json_string(ErrorStatusHandler(), indent); }, + return so->to_json_string(ErrorStatusHandler(), {}, indent); }, "indent"_a = 4) .def("to_json_file", [](SerializableObject* so, std::string file_name, int indent) { - return so->to_json_file(file_name, ErrorStatusHandler(), indent); }, + return so->to_json_file(file_name, ErrorStatusHandler(), {}, indent); }, "file_name"_a, "indent"_a = 4) .def_static("from_json_file", [](std::string file_name) { @@ -200,27 +204,19 @@ static void define_bases2(py::module m) { MarkerVectorProxy::define_py_class(m, "MarkerVector"); EffectVectorProxy::define_py_class(m, "EffectVector"); - py::class_>(m, "Composable", py::dynamic_attr()) - .def(py::init([](std::string const& name, - py::object metadata) { - return new Composable(name, py_to_any_dictionary(metadata)); - }), - py::arg_v("name"_a = std::string()), - py::arg_v("metadata"_a = py::none())) - .def("parent", &Composable::parent) - .def("visible", &Composable::visible) - .def("overlapping", &Composable::overlapping); - auto marker_class = - py::class_>(m, "Marker", py::dynamic_attr()) + py::class_>(m, "Marker", py::dynamic_attr(), R"docstring( +A marker indicates a marked range of time on an item in a timeline, usually with a name, color or other metadata. + +The marked range may have a zero duration. The marked range is in the owning item's time coordinate system. +)docstring") .def(py::init([]( - py::object name, + std::string name, TimeRange marked_range, std::string const& color, py::object metadata) { return new Marker( - string_or_none_converter(name), + name, marked_range, color, py_to_any_dictionary(metadata)); @@ -229,8 +225,8 @@ static void define_bases2(py::module m) { "marked_range"_a = TimeRange(), "color"_a = std::string(Marker::Color::red), py::arg_v("metadata"_a = py::none())) - .def_property("color", &Marker::color, &Marker::set_color) - .def_property("marked_range", &Marker::marked_range, &Marker::set_marked_range); + .def_property("color", &Marker::color, &Marker::set_color, "Color string for this marker (for example: 'RED'), based on the :class:`~Color` enum.") + .def_property("marked_range", &Marker::marked_range, &Marker::set_marked_range, "Range this marker applies to, relative to the :class:`.Item` this marker is attached to (e.g. the :class:`.Clip` or :class:`.Track` that owns this marker)."); py::class_(marker_class, "Color") .def_property_readonly_static("PINK", [](py::object /* self */) { return Marker::Color::pink; }) @@ -249,14 +245,22 @@ static void define_bases2(py::module m) { using SerializableCollectionIterator = ContainerIterator; py::class_(m, "SerializableCollectionIterator", py::dynamic_attr()) .def("__iter__", &SerializableCollectionIterator::iter) - .def("next", &SerializableCollectionIterator::next); + .def("__next__", &SerializableCollectionIterator::next); py::class_>(m, "SerializableCollection", py::dynamic_attr()) - .def(py::init([](std::string const& name, py::object children, + managing_ptr>(m, "SerializableCollection", py::dynamic_attr(), R"docstring( +A container which can hold an ordered list of any serializable objects. Note that this is not a :class:`.Composition` nor is it :class:`.Composable`. + +This container approximates the concept of a bin - a collection of :class:`.SerializableObject`\s that do +not have any compositional meaning, but can serialize to/from OTIO correctly, with metadata and +a named collection. + +A :class:`~SerializableCollection` is useful for serializing multiple timelines, clips, or media references to a single file. +)docstring") + .def(py::init([](std::string const& name, optional> children, py::object metadata) { return new SerializableCollection(name, - py_to_vector(children), + vector_or_default(children), py_to_any_dictionary(metadata)); }), py::arg_v("name"_a = std::string()), "children"_a = py::none(), @@ -286,23 +290,28 @@ static void define_bases2(py::module m) { .def("__iter__", [](SerializableCollection* c) { return new SerializableCollectionIterator(c); }) - .def("clip_if", [](SerializableCollection* t, optional const& search_range) { - return clip_if(t, search_range); - }, "search_range"_a = nullopt) - .def("children_if", [](SerializableCollection* t, py::object descended_from_type, optional const& search_range) { - return children_if(t, descended_from_type, search_range); - }, "descended_from_type"_a = py::none(), "search_range"_a = nullopt); + .def("find_clips", [](SerializableCollection* c, optional const& search_range, bool shallow_search) { + return find_clips(c, search_range, shallow_search); + }, "search_range"_a = nullopt, "shallow_search"_a = false) + .def("find_children", [](SerializableCollection* c, py::object descended_from_type, optional const& search_range, bool shallow_search) { + return find_children(c, descended_from_type, search_range, shallow_search); + }, "descended_from_type"_a = py::none(), "search_range"_a = nullopt, "shallow_search"_a = false); } static void define_items_and_compositions(py::module m) { + auto composable_class = py::class_>(m, "Composable", py::dynamic_attr(), R"docstring( +An object that can be composed within a :class:`~Composition` (such as :class:`~Track` or :class:`.Stack`). +)docstring"); + py::class_>(m, "Item", py::dynamic_attr()) .def(py::init([](std::string name, optional source_range, - py::object effects, py::object markers, py::bool_ enabled, py::object metadata) { + optional> effects, optional> markers, py::bool_ enabled, py::object metadata) { return new Item(name, source_range, py_to_any_dictionary(metadata), - py_to_vector(effects), - py_to_vector(markers), + vector_or_default(effects), + vector_or_default(markers), enabled); }), py::arg_v("name"_a = std::string()), "source_range"_a = nullopt, @@ -310,7 +319,7 @@ static void define_items_and_compositions(py::module m) { "markers"_a = py::none(), "enabled"_a = true, py::arg_v("metadata"_a = py::none())) - .def_property("enabled", &Item::enabled, &Item::set_enabled, "If true, an Item contributes to compositions. Analogous to Mute in various NLEs.") + .def_property("enabled", &Item::enabled, &Item::set_enabled, "If true, an Item contributes to compositions. For example, when an audio/video clip is ``enabled=false`` the clip is muted/hidden.") .def_property("source_range", &Item::source_range, &Item::set_source_range) .def("available_range", [](Item* item) { return item->available_range(ErrorStatusHandler()); @@ -347,7 +356,7 @@ static void define_items_and_compositions(py::module m) { }); auto transition_class = - py::class_>(m, "Transition", py::dynamic_attr()) + py::class_>(m, "Transition", py::dynamic_attr(), "Represents a transition between the two adjacent items in a :class:`.Track`. For example, a cross dissolve or wipe.") .def(py::init([](std::string const& name, std::string const& transition_type, RationalTime in_offset, RationalTime out_offset, py::object metadata) { @@ -359,50 +368,57 @@ static void define_items_and_compositions(py::module m) { "in_offset"_a = RationalTime(), "out_offset"_a = RationalTime(), py::arg_v("metadata"_a = py::none())) - .def_property("transition_type", &Transition::transition_type, &Transition::set_transition_type) - .def_property("in_offset", &Transition::in_offset, &Transition::set_in_offset) - .def_property("out_offset", &Transition::out_offset, &Transition::set_out_offset) + .def_property("transition_type", &Transition::transition_type, &Transition::set_transition_type, "Kind of transition, as defined by the :class:`Type` enum.") + .def_property("in_offset", &Transition::in_offset, &Transition::set_in_offset, "Amount of the previous clip this transition overlaps, exclusive.") + .def_property("out_offset", &Transition::out_offset, &Transition::set_out_offset, "Amount of the next clip this transition overlaps, exclusive.") .def("duration", [](Transition* t) { return t->duration(ErrorStatusHandler()); }) .def("range_in_parent", [](Transition* t) { return t->range_in_parent(ErrorStatusHandler()); - }) + }, "Find and return the range of this item in the parent.") .def("trimmed_range_in_parent", [](Transition* t) { return t->trimmed_range_in_parent(ErrorStatusHandler()); - }); + }, "Find and return the timmed range of this item in the parent."); + py::class_(transition_class, "Type", R"docstring( +Enum encoding types of transitions. - py::class_(transition_class, "Type") +Other effects are handled by the :class:`Effect` class. +)docstring") .def_property_readonly_static("SMPTE_Dissolve", [](py::object /* self */) { return Transition::Type::SMPTE_Dissolve; }) .def_property_readonly_static("Custom", [](py::object /* self */) { return Transition::Type::Custom; }); py::class_>(m, "Gap", py::dynamic_attr()) - .def(py::init([](std::string name, TimeRange source_range, py::object effects, - py::object markers, py::object metadata) { + .def(py::init([](std::string name, TimeRange source_range, optional> effects, + optional> markers, py::object metadata) { return new Gap(source_range, name, - py_to_vector(effects), - py_to_vector(markers), + vector_or_default(effects), + vector_or_default(markers), py_to_any_dictionary(metadata)); }), py::arg_v("name"_a = std::string()), "source_range"_a = TimeRange(), - "effect"_a = py::none(), + "effects"_a = py::none(), "markers"_a = py::none(), py::arg_v("metadata"_a = py::none())) - .def(py::init([](std::string name, RationalTime duration, py::object effects, - py::object markers, py::object metadata) { + .def(py::init([](std::string name, RationalTime duration, optional> effects, + optional> markers, py::object metadata) { return new Gap(duration, name, - py_to_vector(effects), - py_to_vector(markers), + vector_or_default(effects), + vector_or_default(markers), py_to_any_dictionary(metadata)); }), py::arg_v("name"_a = std::string()), "duration"_a = RationalTime(), - "effect"_a = py::none(), + "effects"_a = py::none(), "markers"_a = py::none(), py::arg_v("metadata"_a = py::none())); - auto clip_class = py::class_>(m, "Clip", py::dynamic_attr()) + auto clip_class = py::class_>(m, "Clip", py::dynamic_attr(), R"docstring( +A :class:`~Clip` is a segment of editable media (usually audio or video). + +Contains a :class:`.MediaReference` and a trim on that media reference. +)docstring") .def(py::init([](std::string name, MediaReference* media_reference, optional source_range, py::object metadata, const std::string& active_media_reference) { @@ -428,15 +444,19 @@ static void define_items_and_compositions(py::module m) { using CompositionIterator = ContainerIterator; py::class_(m, "CompositionIterator") .def("__iter__", &CompositionIterator::iter) - .def("next", &CompositionIterator::next); + .def("__next__", &CompositionIterator::next); - py::class_>(m, "Composition", py::dynamic_attr()) + py::class_>(m, "Composition", py::dynamic_attr(), R"docstring( +Base class for an :class:`~Item` that contains :class:`~Composable`\s. + +Should be subclassed (for example by :class:`.Track` and :class:`.Stack`), not used directly. +)docstring") .def(py::init([](std::string name, - py::object children, + optional> children, optional source_range, py::object metadata) { Composition* c = new Composition(name, source_range, py_to_any_dictionary(metadata)); - c->set_children(py_to_vector(children), ErrorStatusHandler()); + c->set_children(vector_or_default(children), ErrorStatusHandler()); return c; }), py::arg_v("name"_a = std::string()), @@ -478,14 +498,14 @@ static void define_items_and_compositions(py::module m) { l.push_back(child.value); } return l; - }) - .def("children_if", [](Composition* t, py::object descended_from_type, optional const& search_range, bool shallow_search) { - return children_if(t, descended_from_type, search_range, shallow_search); + }, "search_range"_a) + .def("find_children", [](Composition* c, py::object descended_from_type, optional const& search_range, bool shallow_search) { + return find_children(c, descended_from_type, search_range, shallow_search); }, "descended_from_type"_a = py::none(), "search_range"_a = nullopt, "shallow_search"_a = false) .def("handles_of_child", [](Composition* c, Composable* child) { auto result = c->handles_of_child(child, ErrorStatusHandler()); return py::make_tuple(py::cast(result.first), py::cast(result.second)); - }, "child_a") + }, "child"_a) .def("has_clips", &Composition::has_clips) .def("__internal_getitem__", [](Composition* c, int index) { index = adjusted_vector_index(index, c->children()); @@ -514,6 +534,17 @@ static void define_items_and_compositions(py::module m) { return new CompositionIterator(c); }); + composable_class + .def(py::init([](std::string const& name, + py::object metadata) { + return new Composable(name, py_to_any_dictionary(metadata)); + }), + py::arg_v("name"_a = std::string()), + py::arg_v("metadata"_a = py::none())) + .def("parent", &Composable::parent) + .def("visible", &Composable::visible) + .def("overlapping", &Composable::overlapping); + auto track_class = py::class_>(m, "Track", py::dynamic_attr()); py::enum_(track_class, "NeighborGapPolicy") @@ -521,12 +552,12 @@ static void define_items_and_compositions(py::module m) { .value("never", Track::NeighborGapPolicy::never); track_class - .def(py::init([](py::object name, py::object children, + .def(py::init([](std::string name, optional> children, optional const& source_range, std::string const& kind, py::object metadata) { - auto composable_children = py_to_vector(children); + auto composable_children = vector_or_default(children); Track* t = new Track( - string_or_none_converter(name), + name, source_range, kind, py_to_any_dictionary(metadata) @@ -545,8 +576,8 @@ static void define_items_and_compositions(py::module m) { auto result = t->neighbors_of(item, ErrorStatusHandler(), policy); return py::make_tuple(py::cast(result.first.take_value()), py::cast(result.second.take_value())); }, "item"_a, "policy"_a = Track::NeighborGapPolicy::never) - .def("clip_if", [](Track* t, optional const& search_range, bool shallow_search) { - return clip_if(t, search_range, shallow_search); + .def("find_clips", [](Track* t, optional const& search_range, bool shallow_search) { + return find_clips(t, search_range, shallow_search); }, "search_range"_a = nullopt, "shallow_search"_a = false); py::class_(track_class, "Kind") @@ -555,24 +586,23 @@ static void define_items_and_compositions(py::module m) { py::class_>(m, "Stack", py::dynamic_attr()) - .def(py::init([](py::object name, - py::object children, + .def(py::init([](std::string name, + optional> children, optional const& source_range, - py::object markers, - py::object effects, + optional> markers, + optional> effects, py::object metadata) { - auto composable_children = py_to_vector(children); + auto composable_children = vector_or_default(children); Stack* s = new Stack( - string_or_none_converter(name), + name, source_range, py_to_any_dictionary(metadata), - py_to_vector(effects), - py_to_vector(markers) + vector_or_default(effects), + vector_or_default(markers) ); if (!composable_children.empty()) { s->set_children(composable_children, ErrorStatusHandler()); } - auto composable_markers = py_to_vector(markers); return s; }), py::arg_v("name"_a = std::string()), @@ -581,16 +611,16 @@ static void define_items_and_compositions(py::module m) { "markers"_a = py::none(), "effects"_a = py::none(), py::arg_v("metadata"_a = py::none())) - .def("clip_if", [](Stack* t, optional const& search_range) { - return clip_if(t, search_range); - }, "search_range"_a = nullopt); + .def("find_clips", [](Stack* s, optional const& search_range, bool shallow_search) { + return find_clips(s, search_range, shallow_search); + }, "search_range"_a = nullopt, "shallow_search"_a = false); py::class_>(m, "Timeline", py::dynamic_attr()) .def(py::init([](std::string name, - py::object children, + optional> children, optional global_start_time, py::object metadata) { - auto composable_children = py_to_vector(children); + auto composable_children = vector_or_default(children); Timeline* t = new Timeline(name, global_start_time, py_to_any_dictionary(metadata)); if (!composable_children.empty()) @@ -611,12 +641,12 @@ static void define_items_and_compositions(py::module m) { }) .def("video_tracks", &Timeline::video_tracks) .def("audio_tracks", &Timeline::audio_tracks) - .def("clip_if", [](Timeline* t, optional const& search_range) { - return clip_if(t, search_range); - }, "search_range"_a = nullopt) - .def("children_if", [](Timeline* t, py::object descended_from_type, optional const& search_range) { - return children_if(t, descended_from_type, search_range); - }, "descended_from_type"_a = py::none(), "search_range"_a = nullopt); + .def("find_clips", [](Timeline* t, optional const& search_range, bool shallow_search) { + return find_clips(t, search_range, shallow_search); + }, "search_range"_a = nullopt, "shallow_search"_a = false) + .def("find_children", [](Timeline* t, py::object descended_from_type, optional const& search_range, bool shallow_search) { + return find_children(t, descended_from_type, search_range, shallow_search); + }, "descended_from_type"_a = py::none(), "search_range"_a = nullopt, "shallow_search"_a = false); } static void define_effects(py::module m) { @@ -630,7 +660,7 @@ static void define_effects(py::module m) { py::arg_v("metadata"_a = py::none())) .def_property("effect_name", &Effect::effect_name, &Effect::set_effect_name); - py::class_>(m, "TimeEffect", py::dynamic_attr()) + py::class_>(m, "TimeEffect", py::dynamic_attr(), "Base class for all effects that alter the timing of an item.") .def(py::init([](std::string name, std::string effect_name, py::object metadata) { @@ -639,7 +669,9 @@ static void define_effects(py::module m) { "effect_name"_a = std::string(), py::arg_v("metadata"_a = py::none())); - py::class_>(m, "LinearTimeWarp", py::dynamic_attr()) + py::class_>(m, "LinearTimeWarp", py::dynamic_attr(), R"docstring( +A time warp that applies a linear speed up or slow down across the entire clip. +)docstring") .def(py::init([](std::string name, double time_scalar, py::object metadata) { @@ -648,9 +680,15 @@ static void define_effects(py::module m) { py::arg_v("name"_a = std::string()), "time_scalar"_a = 1.0, py::arg_v("metadata"_a = py::none())) - .def_property("time_scalar", &LinearTimeWarp::time_scalar, &LinearTimeWarp::set_time_scalar); + .def_property("time_scalar", &LinearTimeWarp::time_scalar, &LinearTimeWarp::set_time_scalar, R"docstring( +Linear time scalar applied to clip. 2.0 means the clip occupies half the time in the parent item, i.e. plays at double speed, +0.5 means the clip occupies twice the time in the parent item, i.e. plays at half speed. - py::class_>(m, "FreezeFrame", py::dynamic_attr()) +Note that adjusting the time_scalar of a :class:`~LinearTimeWarp` does not affect the duration of the item this effect is attached to. +Instead it affects the speed of the media displayed within that item. +)docstring"); + + py::class_>(m, "FreezeFrame", py::dynamic_attr(), "Hold the first frame of the clip for the duration of the clip.") .def(py::init([](std::string name, py::object metadata) { return new FreezeFrame(name, py_to_any_dictionary(metadata)); }), py::arg_v("name"_a = std::string()), @@ -663,7 +701,7 @@ static void define_media_references(py::module m) { .def(py::init([](std::string name, optional available_range, py::object metadata, - optional const& available_image_bounds) { + optional const& available_image_bounds) { return new MediaReference(name, available_range, py_to_any_dictionary(metadata), available_image_bounds); }), py::arg_v("name"_a = std::string()), "available_range"_a = nullopt, @@ -679,7 +717,7 @@ static void define_media_references(py::module m) { .def(py::init([](std::string name, std::string generator_kind, optional const& available_range, py::object parameters, py::object metadata, - optional const& available_image_bounds) { + optional const& available_image_bounds) { return new GeneratorReference(name, generator_kind, available_range, py_to_any_dictionary(parameters), @@ -698,14 +736,18 @@ static void define_media_references(py::module m) { py::class_>(m, "MissingReference", py::dynamic_attr()) + managing_ptr>(m, "MissingReference", py::dynamic_attr(), R"docstring( +Represents media for which a concrete reference is missing. + +Note that a :class:`~MissingReference` may have useful metadata, even if the location of the media is not known. +)docstring") .def(py::init([]( - py::object name, + std::string name, optional available_range, py::object metadata, - optional const& available_image_bounds) { + optional const& available_image_bounds) { return new MissingReference( - string_or_none_converter(name), + name, available_range, py_to_any_dictionary(metadata), available_image_bounds); @@ -721,7 +763,7 @@ static void define_media_references(py::module m) { .def(py::init([](std::string target_url, optional const& available_range, py::object metadata, - optional const& available_image_bounds) { + optional const& available_image_bounds) { return new ExternalReference(target_url, available_range, py_to_any_dictionary(metadata), @@ -734,13 +776,15 @@ static void define_media_references(py::module m) { auto imagesequencereference_class = py:: class_>(m, "ImageSequenceReference", py::dynamic_attr(), R"docstring( -An ImageSequenceReference refers to a numbered series of single-frame image files. Each file can be referred to by a URL generated by the ImageSequenceReference. +An ImageSequenceReference refers to a numbered series of single-frame image files. Each file can be referred to by a URL generated by the :class:`~ImageSequenceReference`. -Image sequncences can have URLs with discontinuous frame numbers, for instance if you've only rendered every other frame in a sequence, your frame numbers may be 1, 3, 5, etc. This is configured using the ``frame_step`` attribute. In this case, the 0th image in the sequence is frame 1 and the 1st image in the sequence is frame 3. Because of this there are two numbering concepts in the image sequence, the image number and the frame number. +Image sequences can have URLs with discontinuous frame numbers, for instance if you've only rendered every other frame in a sequence, your frame numbers may be 1, 3, 5, etc. This is configured using the ``frame_step`` attribute. In this case, the 0th image in the sequence is frame 1 and the 1st image in the sequence is frame 3. Because of this there are two numbering concepts in the image sequence, the image number and the frame number. Frame numbers are the integer numbers used in the frame file name. Image numbers are the 0-index based numbers of the frames available in the reference. Frame numbers can be discontinuous, image numbers will always be zero to the total count of frames minus 1. -An example for 24fps media with a sample provided each frame numbered 1-1000 with a path ``/show/sequence/shot/sample_image_sequence.%04d.exr`` might be:: +An example for 24fps media with a sample provided each frame numbered 1-1000 with a path ``/show/sequence/shot/sample_image_sequence.%04d.exr`` might be + +.. code-block:: json { "available_range": { @@ -762,7 +806,9 @@ An example for 24fps media with a sample provided each frame numbered 1-1000 wit "frame_zero_padding": 4, } -The same duration sequence but with only every 2nd frame available in the sequence would be:: +The same duration sequence but with only every 2nd frame available in the sequence would be + +.. code-block:: json { "available_range": { @@ -784,7 +830,9 @@ The same duration sequence but with only every 2nd frame available in the sequen "frame_zero_padding": 4, } -A list of all the frame URLs in the sequence can be generated, regardless of frame step, with the following list comprehension:: +A list of all the frame URLs in the sequence can be generated, regardless of frame step, with the following list comprehension + +.. code-block:: python [ref.target_url_for_image_number(i) for i in range(ref.number_of_images_in_sequence())] @@ -796,7 +844,7 @@ Negative ``start_frame`` is also handled. The above example with a ``start_frame )docstring"); py::enum_(imagesequencereference_class, "MissingFramePolicy", "Behavior that should be used by applications when an image file in the sequence can't be found on disk.") - .value("error", ImageSequenceReference::MissingFramePolicy::error, "Application should abort and raise an error.") + .value("error", ImageSequenceReference::MissingFramePolicy::error, "Application should stop and raise an error.") .value("hold", ImageSequenceReference::MissingFramePolicy::hold, "Application should hold the last available frame before the missing frame.") .value("black", ImageSequenceReference::MissingFramePolicy::black, "Application should use a black frame in place of the missing frame"); @@ -811,7 +859,7 @@ Negative ``start_frame`` is also handled. The above example with a ``start_frame ImageSequenceReference::MissingFramePolicy const missing_frame_policy, optional const& available_range, py::object metadata, - optional const& available_image_bounds) { + optional const& available_image_bounds) { return new ImageSequenceReference(target_url_base, name_prefix, name_suffix, @@ -841,12 +889,12 @@ Negative ``start_frame`` is also handled. The above example with a ``start_frame .def_property("frame_step", &ImageSequenceReference::frame_step, &ImageSequenceReference::set_frame_step, "Step between frame numbers in file names.") .def_property("rate", &ImageSequenceReference::rate, &ImageSequenceReference::set_rate, "Frame rate if every frame in the sequence were played back.") .def_property("frame_zero_padding", &ImageSequenceReference::frame_zero_padding, &ImageSequenceReference::set_frame_zero_padding, "Number of digits to pad zeros out to in frame numbers.") - .def_property("missing_frame_policy", &ImageSequenceReference::missing_frame_policy, &ImageSequenceReference::set_missing_frame_policy, "Enum ``ImageSequenceReference.MissingFramePolicy`` directive for how frames in sequence not found on disk should be handled.") - .def("end_frame", &ImageSequenceReference::end_frame, "Last frame number in the sequence based on the ``rate`` and ``available_range``.") - .def("number_of_images_in_sequence", &ImageSequenceReference::number_of_images_in_sequence, "Returns the number of images based on the ``rate`` and ``available_range``.") + .def_property("missing_frame_policy", &ImageSequenceReference::missing_frame_policy, &ImageSequenceReference::set_missing_frame_policy, "Directive for how frames in sequence not found during playback or rendering should be handled.") + .def("end_frame", &ImageSequenceReference::end_frame, "Last frame number in the sequence based on the :attr:`rate` and :attr:`.available_range`.") + .def("number_of_images_in_sequence", &ImageSequenceReference::number_of_images_in_sequence, "Returns the number of images based on the :attr:`rate` and :attr:`.available_range`.") .def("frame_for_time", [](ImageSequenceReference *seq_ref, RationalTime time) { return seq_ref->frame_for_time(time, ErrorStatusHandler()); - }, "time"_a, "Given a :class:`RationalTime` within the available range, returns the frame number.") + }, "time"_a, "Given a :class:`.RationalTime` within the available range, returns the frame number.") .def("target_url_for_image_number", [](ImageSequenceReference *seq_ref, int image_number) { return seq_ref->target_url_for_image_number( image_number, @@ -855,14 +903,18 @@ Negative ``start_frame`` is also handled. The above example with a ``start_frame }, "image_number"_a, R"docstring(Given an image number, returns the ``target_url`` for that image. This is roughly equivalent to: - ``f"{target_url_prefix}{(start_frame + (image_number * frame_step)):0{value_zero_padding}}{target_url_postfix}`` + +.. code-block:: python + + f"{target_url_prefix}{(start_frame + (image_number * frame_step)):0{value_zero_padding}}{target_url_postfix}" + )docstring") .def("presentation_time_for_image_number", [](ImageSequenceReference *seq_ref, int image_number) { return seq_ref->presentation_time_for_image_number( image_number, ErrorStatusHandler() ); - }, "image_number"_a, "Given an image number, returns the :class:`RationalTime` at which that image should be shown in the space of `available_range`."); + }, "image_number"_a, "Given an image number, returns the :class:`.RationalTime` at which that image should be shown in the space of :attr:`.available_range`."); } diff --git a/src/py-opentimelineio/opentimelineio-bindings/otio_tests.cpp b/src/py-opentimelineio/opentimelineio-bindings/otio_tests.cpp index d78f3ad5b..218ed856a 100644 --- a/src/py-opentimelineio/opentimelineio-bindings/otio_tests.cpp +++ b/src/py-opentimelineio/opentimelineio-bindings/otio_tests.cpp @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Contributors to the OpenTimelineIO project + #include #include @@ -6,6 +9,8 @@ #include "opentimelineio/serializableCollection.h" #include "opentimelineio/timeline.h" #include "otio_utils.h" +#include "otio_anyDictionary.h" +#include "otio_anyVector.h" namespace py = pybind11; using namespace pybind11::literals; @@ -151,6 +156,15 @@ bool test_big_uint() { return true; } + +void test_AnyDictionary_destroy(AnyDictionaryProxy* d) { + delete d->any_dictionary; +} + +void test_AnyVector_destroy(AnyVectorProxy* v) { + delete v->any_vector; +} + void otio_tests_bindings(py::module m) { TypeRegistry& r = TypeRegistry::instance(); r.register_type(); @@ -168,4 +182,6 @@ void otio_tests_bindings(py::module m) { test.def("gil_scoping", &test_gil_scoping); test.def("xyzzy", &otio_xyzzy); test.def("test_big_uint", &test_big_uint); + test.def("test_AnyDictionary_destroy", &test_AnyDictionary_destroy); + test.def("test_AnyVector_destroy", &test_AnyVector_destroy); } diff --git a/src/py-opentimelineio/opentimelineio-bindings/otio_utils.cpp b/src/py-opentimelineio/opentimelineio-bindings/otio_utils.cpp index ceb6a2167..c327374e3 100644 --- a/src/py-opentimelineio/opentimelineio-bindings/otio_utils.cpp +++ b/src/py-opentimelineio/opentimelineio-bindings/otio_utils.cpp @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Contributors to the OpenTimelineIO project + #include "otio_utils.h" #include "otio_anyDictionary.h" #include "otio_anyVector.h" @@ -92,7 +95,6 @@ void _build_any_to_py_dispatch_table() { } static py::object _value_to_any = py::none(); -static py::object _value_to_so_vector = py::none(); static void py_to_any(py::object const& o, any* result) { if (_value_to_any.is_none()) { @@ -111,41 +113,13 @@ AnyDictionary py_to_any_dictionary(py::object const& o) { any a; py_to_any(o, &a); if (!compare_typeids(a.type(), typeid(AnyDictionary))) { - throw py::type_error(string_printf("expected an AnyDictionary (i.e. metadata); got %s instead", + throw py::type_error(string_printf("Expected an AnyDictionary (i.e. metadata); got %s instead", type_name_for_error_message(a).c_str())); } return safely_cast_any_dictionary_any(a); } -std::vector py_to_so_vector(pybind11::object const& o) { - if (_value_to_so_vector.is_none()) { - py::object core = py::module::import("opentimelineio.core"); - _value_to_so_vector = core.attr("_value_to_so_vector"); - } - - std::vector result; - if (o.is_none()) { - return result; - } - - /* - * We're depending on _value_to_so_vector(), written in Python, to - * not screw up, or we're going to crash. (1) It has to give us - * back an AnyVector. (2) Every element has to be a - * SerializableObject::Retainer<>. - */ - - py::object obj_vector = _value_to_so_vector(o); // need to retain this here or we'll lose the any... - AnyVector const& v = temp_safely_cast_any_vector_any(obj_vector.cast()->a); - - result.reserve(v.size()); - for (auto e: v) { - result.push_back(safely_cast_retainer_any(e)); - } - return result; -} - py::object any_to_py(any const& a, bool top_level) { std::type_info const& tInfo = a.type(); auto e = _py_cast_dispatch_table.find(&tInfo); diff --git a/src/py-opentimelineio/opentimelineio-bindings/otio_utils.h b/src/py-opentimelineio/opentimelineio-bindings/otio_utils.h index f4bbdf308..2a979aabc 100644 --- a/src/py-opentimelineio/opentimelineio-bindings/otio_utils.h +++ b/src/py-opentimelineio/opentimelineio-bindings/otio_utils.h @@ -1,10 +1,13 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Contributors to the OpenTimelineIO project + #pragma once #include #include #include +#include "py-opentimelineio/bindings-common/casters.h" #include "opentimelineio/any.h" -#include "opentimelineio/optional.h" #include "opentimelineio/stringUtils.h" #include "opentimelineio/serializableObject.h" #include "opentimelineio/vectorIndexing.h" @@ -14,13 +17,6 @@ using namespace opentimelineio::OPENTIMELINEIO_VERSION; void install_external_keepalive_monitor(SerializableObject* so, bool apply_now); -namespace pybind11 { namespace detail { - template struct type_caster> - : public optional_caster> {}; - - template<> struct type_caster - : public void_caster {}; -}} template struct managing_ptr { @@ -124,7 +120,6 @@ struct MutableSequencePyAPI : public V { pybind11::class_(m, (name + "Iterator").c_str()) .def("__iter__", &This::Iterator::iter) - .def("next", &This::Iterator::next) .def("__next__", &This::Iterator::next); pybind11::class_(m, name.c_str()) @@ -158,27 +153,5 @@ pybind11::object any_to_py(any const& a, bool top_level = false); pybind11::object plain_string(std::string const& s); pybind11::object plain_int(int i); AnyDictionary py_to_any_dictionary(pybind11::object const& o); -std::vector py_to_so_vector(pybind11::object const& o); bool compare_typeids(std::type_info const& lhs, std::type_info const& rhs); - -template -std::vector py_to_vector(pybind11::object const& o) { - std::vector vso = py_to_so_vector(o); - std::vector result; - - result.reserve(vso.size()); - - for (auto e: vso) { - if (T t = dynamic_cast(e)) { - result.push_back(t); - continue; - } - - throw pybind11::type_error(string_printf("list has element of type %s; expected type %s", - type_name_for_error_message(typeid(*e)).c_str(), - type_name_for_error_message().c_str())); - } - - return result; -} diff --git a/src/py-opentimelineio/opentimelineio/__init__.py b/src/py-opentimelineio/opentimelineio/__init__.py index a3762dd15..036907f4f 100644 --- a/src/py-opentimelineio/opentimelineio/__init__.py +++ b/src/py-opentimelineio/opentimelineio/__init__.py @@ -1,26 +1,5 @@ -# +# SPDX-License-Identifier: Apache-2.0 # Copyright Contributors to the OpenTimelineIO project -# -# Licensed under the Apache License, Version 2.0 (the "Apache License") -# with the following modification; you may not use this file except in -# compliance with the Apache License and the following modification to it: -# Section 6. Trademarks. is deleted and replaced with: -# -# 6. Trademarks. This License does not grant permission to use the trade -# names, trademarks, service marks, or product names of the Licensor -# and its affiliates, except as required to comply with Section 4(c) of -# the License and to reproduce the content of the NOTICE file. -# -# You may obtain a copy of the Apache License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the Apache License with the above modification is -# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -# KIND, either express or implied. See the Apache License for the specific -# language governing permissions and limitations under the Apache License. -# """An editorial interchange format and library. @@ -43,5 +22,6 @@ adapters, hooks, algorithms, - url_utils + url_utils, + versioning, ) diff --git a/src/py-opentimelineio/opentimelineio/adapters/__init__.py b/src/py-opentimelineio/opentimelineio/adapters/__init__.py index 485df8d75..6ed93684d 100644 --- a/src/py-opentimelineio/opentimelineio/adapters/__init__.py +++ b/src/py-opentimelineio/opentimelineio/adapters/__init__.py @@ -1,26 +1,5 @@ -# +# SPDX-License-Identifier: Apache-2.0 # Copyright Contributors to the OpenTimelineIO project -# -# Licensed under the Apache License, Version 2.0 (the "Apache License") -# with the following modification; you may not use this file except in -# compliance with the Apache License and the following modification to it: -# Section 6. Trademarks. is deleted and replaced with: -# -# 6. Trademarks. This License does not grant permission to use the trade -# names, trademarks, service marks, or product names of the Licensor -# and its affiliates, except as required to comply with Section 4(c) of -# the License and to reproduce the content of the NOTICE file. -# -# You may obtain a copy of the Apache License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the Apache License with the above modification is -# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -# KIND, either express or implied. See the Apache License for the specific -# language governing permissions and limitations under the Apache License. -# """Expose the adapter interface to developers. @@ -51,6 +30,20 @@ file_bundle_utils, # utilities for working with OTIO file bundles ) +__all__ = [ + 'Adapter', + 'otio_json', + 'file_bundle_utils', + 'suffixes_with_defined_adapters', + 'available_adapter_names', + 'from_filepath', + 'from_name', + 'read_from_file', + 'read_from_string', + 'write_to_file', + 'write_to_string' +] + def suffixes_with_defined_adapters(read=False, write=False): """Return a set of all the suffixes that have adapters defined for them.""" @@ -91,8 +84,7 @@ def _from_filepath_or_name(filepath, adapter_name): def from_filepath(filepath): """Guess the adapter object to use for a given filepath. - example: - "foo.otio" returns the "otio_json" adapter. + For example, ``foo.otio`` returns the ``otio_json`` adapter. """ outext = os.path.splitext(filepath)[1][1:] @@ -133,7 +125,9 @@ def read_from_file( If adapter_name is None, try and infer the adapter name from the filepath. - For example: + .. code-block:: python + :caption: Example + timeline = read_from_file("example_trailer.otio") timeline = read_from_file("file_with_no_extension", "cmx_3600") """ @@ -160,7 +154,9 @@ def read_from_string( This is useful if you obtain a timeline from someplace other than the filesystem. - Example: + .. code-block:: python + :caption: Example + raw_text = urlopen(my_url).read() timeline = read_from_string(raw_text, "otio_json") """ @@ -185,7 +181,9 @@ def write_to_file( If adapter_name is None, infer the adapter_name to use based on the filepath. - Example: + .. code-block:: python + :caption: Example + otio.adapters.write_to_file(my_timeline, "output.otio") """ @@ -205,7 +203,9 @@ def write_to_string( ): """Return input_otio written to a string using adapter_name. - Example: + .. code-block:: python + :caption: Example + raw_text = otio.adapters.write_to_string(my_timeline, "otio_json") """ diff --git a/src/py-opentimelineio/opentimelineio/adapters/adapter.py b/src/py-opentimelineio/opentimelineio/adapters/adapter.py index 7de25aa23..d614b64a2 100644 --- a/src/py-opentimelineio/opentimelineio/adapters/adapter.py +++ b/src/py-opentimelineio/opentimelineio/adapters/adapter.py @@ -1,31 +1,10 @@ -# +# SPDX-License-Identifier: Apache-2.0 # Copyright Contributors to the OpenTimelineIO project -# -# Licensed under the Apache License, Version 2.0 (the "Apache License") -# with the following modification; you may not use this file except in -# compliance with the Apache License and the following modification to it: -# Section 6. Trademarks. is deleted and replaced with: -# -# 6. Trademarks. This License does not grant permission to use the trade -# names, trademarks, service marks, or product names of the Licensor -# and its affiliates, except as required to comply with Section 4(c) of -# the License and to reproduce the content of the NOTICE file. -# -# You may obtain a copy of the Apache License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the Apache License with the above modification is -# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -# KIND, either express or implied. See the Apache License for the specific -# language governing permissions and limitations under the Apache License. -# """Implementation of the OTIO internal `Adapter` system. For information on writing adapters, please consult: - https://opentimelineio.readthedocs.io/en/latest/tutorials/write-an-adapter.html# # noqa +https://opentimelineio.readthedocs.io/en/latest/tutorials/write-an-adapter.html # noqa """ import inspect @@ -40,20 +19,15 @@ ) -try: - # Python 3.0+ - getfullargspec = inspect.getfullargspec -except AttributeError: - getfullargspec = inspect.getargspec - - @core.register_type class Adapter(plugins.PythonPlugin): """Adapters convert between OTIO and other formats. - Note that this class is not subclassed by adapters. Rather, an adapter is + Note that this class is not subclassed by adapters. Rather, an adapter is a python module that implements at least one of the following functions: + .. code-block:: python + write_to_string(input_otio) write_to_file(input_otio, filepath) (optionally inferred) read_from_string(input_str) @@ -64,22 +38,19 @@ class Adapter(plugins.PythonPlugin): to OTIO. You should not need to extend this class to create new adapters for OTIO. - For more information: - https://opentimelineio.readthedocs.io/en/latest/tutorials/write-an-adapter.html# # noqa + For more information: https://opentimelineio.readthedocs.io/en/latest/tutorials/write-an-adapter.html. # noqa """ _serializable_label = "Adapter.1" def __init__( self, name=None, - execution_scope=None, filepath=None, suffixes=None ): plugins.PythonPlugin.__init__( self, name, - execution_scope, filepath ) @@ -96,7 +67,7 @@ def has_feature(self, feature_string): return true if adapter supports feature_string, which must be a key of the _FEATURE_MAP dictionary. - Will trigger a call to self.module(), which imports the plugin. + Will trigger a call to :meth:`.PythonPlugin.module`, which imports the plugin. """ if feature_string.lower() not in _FEATURE_MAP: @@ -134,7 +105,7 @@ def read_from_file( not self.has_feature("read_from_file") and self.has_feature("read_from_string") ): - with open(filepath, 'r') as fo: + with open(filepath) as fo: contents = fo.read() result = self._execute_function( "read_from_string", @@ -311,11 +282,9 @@ def __str__(self): "Adapter(" "{}, " "{}, " - "{}, " "{}" ")".format( repr(self.name), - repr(self.execution_scope), repr(self.filepath), repr(self.suffixes), ) @@ -325,12 +294,10 @@ def __repr__(self): return ( "otio.adapter.Adapter(" "name={}, " - "execution_scope={}, " "filepath={}, " "suffixes={}" ")".format( repr(self.name), - repr(self.execution_scope), repr(self.filepath), repr(self.suffixes), ) @@ -339,7 +306,7 @@ def __repr__(self): def plugin_info_map(self): """Adds extra adapter-specific information to call to the parent fn.""" - result = super(Adapter, self).plugin_info_map() + result = super().plugin_info_map() features = collections.OrderedDict() result["supported features"] = features @@ -355,7 +322,7 @@ def plugin_info_map(self): for fn_name in _FEATURE_MAP[feature]: if hasattr(self.module(), fn_name): fn = getattr(self.module(), fn_name) - args = getfullargspec(fn) + args = inspect.getfullargspec(fn) docs = inspect.getdoc(fn) break @@ -379,13 +346,13 @@ def _with_linked_media_references( if not read_otio or not media_linker.from_name(media_linker_name): return read_otio - # not every object the adapter reads has an "each_clip" method, so this + # not every object the adapter reads has an "find_clips" method, so this # skips objects without one. - clpfn = getattr(read_otio, "each_clip", None) + clpfn = getattr(read_otio, "find_clips", None) if clpfn is None: return read_otio - for cl in read_otio.each_clip(): + for cl in read_otio.find_clips(): new_mr = media_linker.linked_media_reference( cl, media_linker_name, diff --git a/src/py-opentimelineio/opentimelineio/adapters/builtin_adapters.plugin_manifest.json b/src/py-opentimelineio/opentimelineio/adapters/builtin_adapters.plugin_manifest.json index 0662e7ba2..2614e36b8 100644 --- a/src/py-opentimelineio/opentimelineio/adapters/builtin_adapters.plugin_manifest.json +++ b/src/py-opentimelineio/opentimelineio/adapters/builtin_adapters.plugin_manifest.json @@ -4,42 +4,36 @@ { "OTIO_SCHEMA": "Adapter.1", "name": "fcp_xml", - "execution_scope": "in process", "filepath": "fcp_xml.py", "suffixes": ["xml"] }, { "OTIO_SCHEMA" : "Adapter.1", "name" : "otio_json", - "execution_scope" : "in process", "filepath" : "otio_json.py", "suffixes" : ["otio"] }, { "OTIO_SCHEMA" : "Adapter.1", "name" : "otioz", - "execution_scope" : "in process", "filepath" : "otioz.py", "suffixes" : ["otioz"] }, { "OTIO_SCHEMA" : "Adapter.1", "name" : "otiod", - "execution_scope" : "in process", "filepath" : "otiod.py", "suffixes" : ["otiod"] }, { "OTIO_SCHEMA" : "Adapter.1", "name" : "cmx_3600", - "execution_scope" : "in process", "filepath" : "cmx_3600.py", "suffixes" : ["edl"] }, { "OTIO_SCHEMA" : "Adapter.1", "name" : "svg", - "execution_scope" : "in process", "filepath" : "svg.py", "suffixes" : ["svg"] } @@ -49,5 +43,7 @@ "post_media_linker" : [], "pre_adapter_write" : [], "post_adapter_write" : [] + }, + "version_manifests": { } } diff --git a/src/py-opentimelineio/opentimelineio/adapters/cmx_3600.py b/src/py-opentimelineio/opentimelineio/adapters/cmx_3600.py index 07e451864..de5df07c4 100644 --- a/src/py-opentimelineio/opentimelineio/adapters/cmx_3600.py +++ b/src/py-opentimelineio/opentimelineio/adapters/cmx_3600.py @@ -1,26 +1,5 @@ -# +# SPDX-License-Identifier: Apache-2.0 # Copyright Contributors to the OpenTimelineIO project -# -# Licensed under the Apache License, Version 2.0 (the "Apache License") -# with the following modification; you may not use this file except in -# compliance with the Apache License and the following modification to it: -# Section 6. Trademarks. is deleted and replaced with: -# -# 6. Trademarks. This License does not grant permission to use the trade -# names, trademarks, service marks, or product names of the Licensor -# and its affiliates, except as required to comply with Section 4(c) of -# the License and to reproduce the content of the NOTICE file. -# -# You may obtain a copy of the Apache License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the Apache License with the above modification is -# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -# KIND, either express or implied. See the Apache License for the specific -# language governing permissions and limitations under the Apache License. -# """OpenTimelineIO CMX 3600 EDL Adapter""" @@ -36,7 +15,6 @@ import copy import os import re -import math import collections from .. import ( @@ -86,16 +64,23 @@ class EDLParseError(exceptions.OTIOError): # the comment string for the media reference: # 'avid': '* FROM CLIP:' (default) # 'nucoda': '* FROM FILE:' +# 'premiere': None (If Adobe Premiere imports an EDL that uses +# a "FROM" comment will result in the clips +# being named UNKNOWN instead of using the reel or file name) # When adding a new style, please be sure to add sufficient tests # to verify both the new and existing styles. -VALID_EDL_STYLES = ['avid', 'nucoda'] +VALID_EDL_STYLES = { + 'avid': 'CLIP', + 'nucoda': 'FILE', + 'premiere': None, +} def _extend_source_range_duration(obj, duration): obj.source_range = obj.source_range.duration_extended_by(duration) -class EDLParser(object): +class EDLParser: def __init__(self, edl_string, rate=24, ignore_timecode_mismatch=False): self.timeline = schema.Timeline() @@ -109,10 +94,17 @@ def __init__(self, edl_string, rate=24, ignore_timecode_mismatch=False): # TODO: Sort the tracks V, then A1,A2,etc. - def add_clip(self, line, comments, rate=24): + def add_clip(self, line, comments, rate=24, transition_line=None): comment_handler = CommentHandler(comments) - clip_handler = ClipHandler(line, comment_handler.handled, rate=rate) + clip_handler = ClipHandler( + line, + comment_handler.handled, + rate=rate, + transition_line=transition_line + ) clip = clip_handler.clip + transition = clip_handler.transition + # Add unhandled comments as general comments to meta data. if comment_handler.unhandled: clip.metadata.setdefault("cmx_3600", {}) clip.metadata['cmx_3600'].setdefault("comments", []) @@ -129,15 +121,6 @@ def add_clip(self, line, comments, rate=24): clip.metadata.setdefault("cmx_3600", {}) clip.metadata['cmx_3600']['reel'] = clip_handler.reel - # each edit point between two clips is a transition. the default is a - # cut in the edl format the transition codes are for the transition - # into the clip - self.add_transition( - clip_handler, - clip_handler.transition_type, - clip_handler.transition_data - ) - edl_rate = clip_handler.edl_rate record_in = opentime.from_timecode( clip_handler.record_tc_in, @@ -195,8 +178,11 @@ def add_clip(self, line, comments, rate=24): # Add clip instances to the tracks tracks = self.tracks_for_channel(clip_handler.channel_code) for track in tracks: + track_transition = transition if len(tracks) > 1: track_clip = copy.deepcopy(clip) + if transition: + track_transition = copy.deepcopy(transition) else: track_clip = clip @@ -234,6 +220,12 @@ def add_clip(self, line, comments, rate=24): track.append(gap) _extend_source_range_duration(track, gap.duration()) + if track_transition: + if len(track) < 1: + raise EDLParseError( + "Transitions can't be at the very beginning of a track" + ) + track.append(track_transition) track.append(track_clip) _extend_source_range_duration(track, track_clip.duration()) @@ -264,12 +256,6 @@ def tracks_for_channel(self, channel_code): # Return a list of actual tracks return [self.tracks_by_name[c] for c in track_names] - def add_transition(self, clip_handler, transition, data): - if transition not in ['C']: - md = clip_handler.clip.metadata.setdefault("cmx_3600", {}) - md["transition"] = transition - md["transition_duration"] = float(data) - def parse_edl(self, edl_string, rate=24): # edl 'events' can be comprised of an indeterminate amount of lines # we are to translating 'events' to a single clip and transition @@ -322,7 +308,7 @@ def parse_edl(self, edl_string, rate=24): line_1 = edl_lines.pop(0) line_2 = edl_lines.pop(0) - + # TODO: check if transitions can happen in this case comments = [] while edl_lines: if re.match(r'^\D', edl_lines[0]): @@ -331,24 +317,45 @@ def parse_edl(self, edl_string, rate=24): break self.add_clip(line_1, comments, rate=rate) self.add_clip(line_2, comments, rate=rate) - + # Check if the first character in the line is a digit elif line[0].isdigit(): + transition_line = None # all 'events' start_time with an edit decision. this is # denoted by the line beginning with a padded integer 000-999 comments = [] + event_id = int(re.match(r'^\d+', line).group(0)) while edl_lines: - # any non-numbered lines after an edit decision should be - # treated as 'comments' - # comments are string tags used by the reader to get extra + # Any non-numbered lines after an edit decision should be + # treated as 'comments'. + # Comments are string tags used by the reader to get extra # information not able to be found in the restricted edl - # format - if re.match(r'^\D', edl_lines[0]): + # format. + # If the current event id is repeated it means that there is + # a transition between the current event and the preceding + # one. We collect it and process it when adding the clip. + m = re.match(r'^\d+', edl_lines[0]) + if not m: comments.append(edl_lines.pop(0)) else: - break - - self.add_clip(line, comments, rate=rate) - + if int(m.group(0)) == event_id: + # It is not possible to have multiple transitions + # for the same event. + if transition_line: + raise EDLParseError( + 'Invalid transition %s' % edl_lines[0] + ) + # Same event id, this is a transition + transition_line = edl_lines.pop(0) + else: + # New event, stop collecting comments and transitions + break + + self.add_clip( + line, + comments, + rate=rate, + transition_line=transition_line + ) else: raise EDLParseError('Unknown event type') @@ -359,26 +366,32 @@ def parse_edl(self, edl_string, rate=24): track.source_range = None -class ClipHandler(object): +class ClipHandler: # /path/filename.[1001-1020].ext image_sequence_pattern = re.compile( r'.*\.(?P\[(?P[0-9]+)-(?P[0-9]+)\])\.\w+$' ) - def __init__(self, line, comment_data, rate=24): + def __init__(self, line, comment_data, rate=24, transition_line=None): self.clip_num = None self.reel = None self.channel_code = None self.edl_rate = rate self.transition_id = None self.transition_data = None + self.transition_type = None self.source_tc_in = None self.source_tc_out = None self.record_tc_in = None self.record_tc_out = None - + self.clip = None + self.transition = None self.parse(line) + if transition_line: + self.parse(transition_line) self.clip = self.make_clip(comment_data) + if transition_line: + self.transition = self.make_transition(comment_data) def is_image_sequence(self, comment_data): return self.image_sequence_pattern.search( @@ -413,7 +426,7 @@ def make_clip(self, comment_data): # BLACK/BL and BARS are called out as "Special Source Identifiers" in # the documents referenced here: - # https://github.com/PixarAnimationStudios/OpenTimelineIO#cmx3600-edl + # https://github.com/AcademySoftwareFoundation/OpenTimelineIO#cmx3600-edl if self.reel in ['BL', 'BLACK']: clip.media_reference = schema.GeneratorReference() # TODO: Replace with enum, once one exists @@ -483,7 +496,7 @@ def make_clip(self, comment_data): power = [floats[6], floats[7], floats[8]] else: raise EDLParseError( - 'Invalid ASC_SOP found: {}'.format(asc_sop)) + f'Invalid ASC_SOP found: {asc_sop}') if asc_sat: sat = float(asc_sat) @@ -497,11 +510,8 @@ def make_clip(self, comment_data): } } - # In transitions, some of the source clip metadata might fall in the - # transition clip event + # Get the clip name from "TO CLIP NAME" if present if 'dest_clip_name' in comment_data: - previous_meta = clip.metadata.setdefault('previous_metadata', {}) - previous_meta['source_clip_name'] = clip.name clip.name = comment_data['dest_clip_name'] if 'locators' in comment_data: @@ -571,7 +581,7 @@ def parse(self, line): # denotes frame count # i haven't figured out how the key transitions (K, KB, KO) work ( - self.clip_num, + self.transition_id, self.reel, self.channel_code, self.transition_type, @@ -583,22 +593,29 @@ def parse(self, line): ) = fields elif field_count == 8: + edit_type = None # no transition data # this is for basic cuts ( self.clip_num, self.reel, self.channel_code, - self.transition_type, + edit_type, self.source_tc_in, self.source_tc_out, self.record_tc_in, self.record_tc_out ) = fields - + # Double check it is a cut + if edit_type not in ["C"]: + raise EDLParseError( + 'incorrect edit type {} in form statement: {}'.format( + edit_type, line, + ) + ) else: raise EDLParseError( - 'incorrect number of fields [{0}] in form statement: {1}' + 'incorrect number of fields [{}] in form statement: {}' ''.format(field_count, line)) # Frame numbers (not just timecode) are ok @@ -621,8 +638,77 @@ def parse(self, line): ) ) + def make_transition(self, comment_data): + # Do some sanity check + if not self.clip: + raise RuntimeError("Transitions can't be handled without a clip") + if self.transition_id != self.clip_num: + raise EDLParseError( + 'transition and event id mismatch: {} vs {}'.format( + self.transaction_id, self.clip_num, + ) + ) + if re.match(r'W(\d{3})', self.transition_type): + otio_transition_type = "SMPTE_Wipe" + elif self.transition_type in ['D']: + otio_transition_type = schema.TransitionTypes.SMPTE_Dissolve + else: + raise EDLParseError( + "Transition type '{}' not supported by the CMX EDL reader " + "currently.".format(self.transition_type) + ) + # TODO: support delayed transition like described here: + # https://xmil.biz/EDL-X/CMX3600.pdf + transition_duration = opentime.RationalTime( + float(self.transition_data), + self.clip.source_range.duration.rate + ) + # Note: Transitions in EDLs are unconventionally represented. + # + # Where a transition might normally be visualized like: + # |---57.0 Trans 43.0----| + # |------Clip1 102.0------|----------Clip2 143.0----------|Clip3 24.0| + # + # In an EDL it can be thought of more like this: + # |---0.0 Trans 100.0----| + # |Clip1 45.0|----------------Clip2 200.0-----------------|Clip3 24.0| + # + # So the transition starts at the beginning of the clip with `duration` + # frames from the previous clip. + + # Give the transition a detailed name if we can + transition_name = '{} to {}'.format( + otio_transition_type, + self.clip.name, + ) + if 'dest_clip_name' in comment_data: + if 'clip_name' in comment_data: + transition_name = '{} from {} to {}'.format( + otio_transition_type, + comment_data['clip_name'], + comment_data['dest_clip_name'], + ) + + new_trx = schema.Transition( + name=transition_name, + # only supported type at the moment + transition_type=otio_transition_type, + metadata={ + 'cmx_3600': { + 'transition': self.transition_type, + 'transition_duration': transition_duration.value, + } + }, + ) + new_trx.in_offset = opentime.RationalTime( + 0, + transition_duration.rate + ) + new_trx.out_offset = transition_duration + return new_trx + -class CommentHandler(object): +class CommentHandler: # this is the for that all comment 'id' tags take regex_template = r'\*?\s*{id}:?\s*(?P.*)' @@ -640,6 +726,7 @@ class CommentHandler(object): ('ASC_SAT', 'asc_sat'), ('M2', 'motion_effect'), ('\\* FREEZE FRAME', 'freeze_frame'), + ('\\* OTIO REFERENCE [a-zA-Z]+', 'media_reference'), ]) def __init__(self, comments): @@ -672,218 +759,6 @@ def parse(self, comment): self.unhandled.append(stripped) -def _get_next_clip(start_index, track): - """Get the next clip with a non-zero duration""" - # Iterate over the following clips and return the first "real" one - for clip in track[start_index + 1:]: - if clip.duration().value > 0: - return clip - - return None - - -def _expand_transitions(timeline): - """Convert clips with metadata/transition == 'D' into OTIO transitions.""" - - tracks = timeline.tracks - remove_list = [] - replace_or_insert_list = [] - append_list = [] - for track in tracks: - # avid inserts an extra clip for the source - prev_prev = None - prev = None - for index, clip in enumerate(track): - transition_type = clip.metadata.get('cmx_3600', {}).get( - 'transition', - 'C' - ) - - if transition_type == 'C': - # nothing to do, continue to the next iteration of the loop - prev_prev = prev - prev = clip - continue - - wipe_match = re.match(r'W(\d{3})', transition_type) - if wipe_match is not None: - otio_transition_type = "SMPTE_Wipe" - elif transition_type in ['D']: - otio_transition_type = schema.TransitionTypes.SMPTE_Dissolve - else: - raise EDLParseError( - "Transition type '{}' not supported by the CMX EDL reader " - "currently.".format(transition_type) - ) - - # Using transition data for duration (with clip duration as backup.) - # Link: https://ieeexplore.ieee.org/document/7291839 - # Citation: "ST 258:2004 - SMPTE Standard - For Television - Transfer - # of Edit Decision Lists," in ST 258:2004 , vol., no., pp.1-37, - # 6 April 2004, doi: 10.5594/SMPTE.ST258.2004. - if clip.metadata.get("cmx_3600", {}).get("transition_duration"): - transition_duration = opentime.RationalTime( - clip.metadata["cmx_3600"]["transition_duration"], - clip.duration().rate - ) - else: - transition_duration = clip.duration() - - # EDL doesn't have enough data to know where the cut point was, so - # this arbitrarily puts it in the middle of the transition - pre_cut = math.floor(transition_duration.value / 2) - post_cut = transition_duration.value - pre_cut - mid_tran_cut_pre_duration = opentime.RationalTime( - pre_cut, - transition_duration.rate - ) - mid_tran_cut_post_duration = opentime.RationalTime( - post_cut, - transition_duration.rate - ) - - # Because transitions can have two event entries followed by - # comments, some of the previous clip's metadata might land in the - # transition clip - if prev: - if 'previous_metadata' in clip.metadata: - prev_metadata = clip.metadata['previous_metadata'] - if 'source_clip_name' in prev_metadata: - # Give the transition the event name and the - # previous clip the appropriate name - prev.name = prev_metadata['source_clip_name'] - - # expand the previous - expansion_clip = None - if prev and not prev_prev: - expansion_clip = prev - elif prev_prev: - # If the previous clip is continuous to this one, we can combine - if _transition_clips_continuous(prev_prev, prev): - expansion_clip = prev_prev - if prev: - remove_list.append((track, prev)) - else: - expansion_clip = prev - - _extend_source_range_duration(expansion_clip, mid_tran_cut_pre_duration) - - # rebuild the clip as a transition - new_trx = schema.Transition( - name=clip.name, - # only supported type at the moment - transition_type=otio_transition_type, - metadata=clip.metadata, - ) - new_trx.in_offset = mid_tran_cut_pre_duration - new_trx.out_offset = mid_tran_cut_post_duration - - # expand the next_clip or contract this clip - keep_transition_clip = False - next_clip = _get_next_clip(index, track) - if next_clip: - if _transition_clips_continuous(clip, next_clip): - sr = next_clip.source_range - next_clip.source_range = opentime.TimeRange( - sr.start_time - mid_tran_cut_post_duration, - sr.duration + mid_tran_cut_post_duration, - ) - else: - # The clip was only expressed in the transition, keep it, - # though it needs the previous clip transition time removed - keep_transition_clip = True - - sr = clip.source_range - clip.source_range = opentime.TimeRange( - sr.start_time + mid_tran_cut_pre_duration, - sr.duration - mid_tran_cut_pre_duration, - ) - else: - fill = schema.Gap( - source_range=opentime.TimeRange( - duration=mid_tran_cut_post_duration, - start_time=opentime.RationalTime( - 0, - transition_duration.rate - ) - ) - ) - append_list.append((track, fill)) - - # in from to - replace_or_insert_list.append((keep_transition_clip, track, clip, new_trx)) - - # Scrub some temporary metadata stashed on clips about their - # neighbors - if 'previous_metadata' in clip.metadata: - del(clip.metadata['previous_metadata']) - - if 'previous_metadata' in new_trx.metadata: - del(new_trx.metadata['previous_metadata']) - - prev = clip - - for (insert, track, from_clip, to_transition) in replace_or_insert_list: - clip_index = track.index(from_clip) - if insert: - track.insert(clip_index, to_transition) - else: - track[clip_index] = to_transition - - for (track, clip_to_remove) in list(set(remove_list)): - # if clip_to_remove in track: - track.remove(clip_to_remove) - - for (track, clip) in append_list: - track.append(clip) - - return timeline - - -def _transition_clips_continuous(clip_a, clip_b): - """Tests if two clips are continuous. They are continuous if the following - conditions are met: - 1. clip_a's source range ends on the last frame before clip_b's - 2a. If clip_a's name matches clip_b's - - or - - 2b. clip_a name matches metadata source_clip_name in clip_b - - or - - 2c. Reel name matches - - or - - 2d. Both clips are gaps - - - This is specific to how this adapter parses EDLs and is meant to be run only - within _expand_transitions. - """ - clip_a_end = clip_a.source_range.end_time_exclusive() - if not clip_a_end == clip_b.source_range.start_time: - return False - - if all(isinstance(clip, schema.Gap) for clip in (clip_a, clip_b)): - return True - - # The time ranges are continuous, match the names - if (clip_a.name == clip_b.name): - return True - - def reelname(clip): - return clip.metadata['cmx_3600']['reel'] - - try: - if reelname(clip_a) == reelname(clip_b): - return True - except KeyError: - pass - - try: - return clip_a.name == clip_b.metadata['previous_metadata']['source_clip_name'] - except KeyError: - pass - - return False - - def read_from_string(input_str, rate=24, ignore_timecode_mismatch=False): """Reads a CMX Edit Decision List (EDL) from a string. Since EDLs don't contain metadata specifying the rate they are meant @@ -915,7 +790,6 @@ def read_from_string(input_str, rate=24, ignore_timecode_mismatch=False): ignore_timecode_mismatch=ignore_timecode_mismatch ) result = parser.timeline - result = _expand_transitions(result) return result @@ -958,7 +832,7 @@ def write_to_string(input_otio, rate=None, style='avid', reelname_len=8): return writer.get_content_for_track_at_index(0, title=input_otio.name) -class EDLWriter(object): +class EDLWriter: def __init__(self, tracks, rate, style, reelname_len=8): self._tracks = tracks self._rate = rate @@ -1056,7 +930,7 @@ def get_content_for_track_at_index(self, idx, title): # needed. pass - content = "TITLE: {}\n\n".format(title) if title else '' + content = f"TITLE: {title}\n\n" if title else '' if track.enabled: # Convert each event/dissolve-event into plain text. for idx, event in enumerate(events): @@ -1095,7 +969,7 @@ def _relevant_timing_effect(clip): return timing_effect -class Event(object): +class Event: def __init__( self, clip, @@ -1106,7 +980,13 @@ def __init__( reelname_len ): - line = EventLine(kind, rate, reel=_reel_from_clip(clip, reelname_len)) + # Premiere style uses AX for the reel name + if style == 'premiere': + reel = 'AX' + else: + reel = _reel_from_clip(clip, reelname_len) + + line = EventLine(kind, rate, reel=reel) line.source_in = clip.source_range.start_time line.source_out = clip.source_range.end_time_exclusive() @@ -1164,7 +1044,7 @@ def to_edl_format(self): return "\n".join(lines) -class DissolveEvent(object): +class DissolveEvent: def __init__( self, @@ -1283,7 +1163,7 @@ def to_edl_format(self): return "\n".join(lines) -class EventLine(object): +class EventLine: def __init__(self, kind, rate, reel='AX'): self.reel = reel self._kind = 'V' if kind == schema.TrackKind.Video else 'A' @@ -1346,6 +1226,10 @@ def _generate_comment_lines( elif hasattr(clip.media_reference, 'abstract_target_url'): url = _get_image_sequence_url(clip) + if url: + # Premiere style uses the base name of the media reference + if style == 'premiere': + clip.name = os.path.basename(clip.media_reference.target_url) else: url = clip.name @@ -1374,28 +1258,37 @@ def _generate_comment_lines( lines.append( "* {from_or_to} CLIP NAME: {name}{suffix}".format( from_or_to=from_or_to, - name=clip.name, + name=os.path.basename(url) if style == 'premiere' else clip.name, suffix=suffix ) ) if timing_effect and timing_effect.effect_name == "FreezeFrame": lines.append('* * FREEZE FRAME') - if url and style == 'avid': - lines.append("* {from_or_to} CLIP: {url}".format( - from_or_to=from_or_to, - url=url - )) - if url and style == 'nucoda': - lines.append("* {from_or_to} FILE: {url}".format( - from_or_to=from_or_to, - url=url - )) + + # If the style has a spec, apply it and add it as a comment + style_spec = VALID_EDL_STYLES.get(style) + if url: + if style_spec: + lines.append("* {from_or_to} {style_spec}: {url}".format( + from_or_to=from_or_to, + style_spec=style_spec, + url=_flip_windows_slashes(url) + )) + else: + lines.append("* OTIO REFERENCE {from_or_to}: {url}".format( + from_or_to=from_or_to, + url=_flip_windows_slashes(url) + )) if reelname_len and not clip.metadata.get('cmx_3600', {}).get('reel'): lines.append("* OTIO TRUNCATED REEL NAME FROM: {url}".format( url=os.path.basename(_flip_windows_slashes(url or clip.name)) )) + if style == 'premiere': + clip.metadata.setdefault('cmx_3600', {}) + clip.metadata['cmx_3600'].update({'reel': 'AX'}) + cdl = clip.metadata.get('cdl') if cdl: asc_sop = cdl.get('asc_sop') @@ -1430,13 +1323,13 @@ def _generate_comment_lines( if not color and meta and meta.get("color"): color = meta.get("color").upper() comment = (marker.name or '').upper() - lines.append("* LOC: {} {:7} {}".format(timecode, color, comment)) + lines.append(f"* LOC: {timecode} {color:7} {comment}") # If we are carrying any unhandled CMX 3600 comments on this clip # then output them blindly. extra_comments = clip.metadata.get('cmx_3600', {}).get('comments', []) for comment in extra_comments: - lines.append("* {}".format(comment)) + lines.append(f"* {comment}") return lines diff --git a/src/py-opentimelineio/opentimelineio/adapters/fcp_xml.py b/src/py-opentimelineio/opentimelineio/adapters/fcp_xml.py index 9465128b7..34e420768 100644 --- a/src/py-opentimelineio/opentimelineio/adapters/fcp_xml.py +++ b/src/py-opentimelineio/opentimelineio/adapters/fcp_xml.py @@ -1,26 +1,5 @@ -# +# SPDX-License-Identifier: Apache-2.0 # Copyright Contributors to the OpenTimelineIO project -# -# Licensed under the Apache License, Version 2.0 (the "Apache License") -# with the following modification; you may not use this file except in -# compliance with the Apache License and the following modification to it: -# Section 6. Trademarks. is deleted and replaced with: -# -# 6. Trademarks. This License does not grant permission to use the trade -# names, trademarks, service marks, or product names of the Licensor -# and its affiliates, except as required to comply with Section 4(c) of -# the License and to reproduce the content of the NOTICE file. -# -# You may obtain a copy of the Apache License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the Apache License with the above modification is -# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -# KIND, either express or implied. See the Apache License for the specific -# language governing permissions and limitations under the Apache License. -# """OpenTimelineIO Final Cut Pro 7 XML Adapter.""" @@ -33,22 +12,8 @@ from xml.etree import cElementTree from xml.dom import minidom -# urlparse's name changes in Python 3 -try: - # Python 2.7 - import urlparse as urllib_parse -except ImportError: - # Python 3 - basestring = str - import urllib.parse as urllib_parse - -# Same with the ABC classes from collections -try: - # Python 3 - from collections.abc import Mapping -except ImportError: - # Python 2.7 - from collections import Mapping +import urllib.parse as urllib_parse +from collections.abc import Mapping from .. import ( core, @@ -142,7 +107,7 @@ def context_pushing_element(self, element): for context_element in self.elements: if context_element == element: raise ValueError( - "element {} already in context".format(element) + f"element {element} already in context" ) return _Context(element, self.elements) @@ -169,10 +134,10 @@ def _element_identification_string(element): Gets a string that will hopefully help in identifing an element when there is an error. """ - info_string = "tag: {}".format(element.tag) + info_string = f"tag: {element.tag}" try: elem_id = element.attrib["id"] - info_string += " id: {}".format(elem_id) + info_string += f" id: {elem_id}" except KeyError: pass @@ -280,7 +245,7 @@ def _track_kind_from_element(media_element): elif element_tag == "video": return schema.TrackKind.Video - raise ValueError("Unsupported media kind: {}".format(media_element.tag)) + raise ValueError(f"Unsupported media kind: {media_element.tag}") def _is_primary_audio_channel(track): @@ -360,7 +325,7 @@ def _xml_tree_to_dict(node, ignore_tags=None, omit_timing=True): # Handle the attributes out_dict.update( collections.OrderedDict( - ("@{}".format(k), v) for k, v in node.attrib.items() + (f"@{k}", v) for k, v in node.attrib.items() ) ) @@ -443,7 +408,7 @@ def elements_for_value(python_value, element_tag): pass # test for list-like objects (but not string-derived) - if not isinstance(python_value, basestring): + if not isinstance(python_value, str): try: iter(python_value) return itertools.chain.from_iterable( @@ -582,9 +547,7 @@ class FCP7XMLParser: 1. Inheritance 2. The id Attribute - .. seealso:: https://developer.apple.com/library/archive/documentation/\ - AppleApplications/Reference/FinalCutPro_XML/Basics/Basics.html\ - #//apple_ref/doc/uid/TP30001154-TPXREF102 + .. seealso:: https://developer.apple.com/library/archive/documentation/AppleApplications/Reference/FinalCutPro_XML/Basics/Basics.html#//apple_ref/doc/uid/TP30001154-TPXREF102 # noqa Inheritance is implemented using a _Context object that is pushed down through layers of parsing. A given parsing method is passed the element to @@ -775,7 +738,7 @@ def track_for_element(self, track_element, track_kind, context): """ local_context = context.context_pushing_element(track_element) name_element = track_element.find("./name") - track_name = (name_element.text if name_element is not None else None) + track_name = (name_element.text if name_element is not None else '') timeline_item_tags = {"clipitem", "generatoritem", "transitionitem"} @@ -1013,7 +976,7 @@ def item_and_timing_for_element( elif item_element.tag == "transitionitem": item = self.transition_for_element(item_element, context) else: - name = "unknown-{}".format(item_element.tag) + name = f"unknown-{item_element.tag}" item = core.Item(name=name, source_range=item_range) if metadata_dict: @@ -1124,7 +1087,7 @@ def effect_from_filter_element(self, filter_element): if effect_element is None: raise ValueError( - "could not find effect in filter: {}".format(filter_element) + f"could not find effect in filter: {filter_element}" ) name = _name_from_element(effect_element) @@ -1195,7 +1158,7 @@ def _backreference_for_item(item, tag, br_map): # of hash to id int as values. def id_string(id_int): - return "{}-{}".format(tag, id_int) + return f"{tag}-{id_int}" # Determine how to uniquely identify the referenced item if isinstance(item, schema.ExternalReference): @@ -1386,7 +1349,7 @@ def _build_timecode(time, fps, drop_frame=False, additional_metadata=None): frame_number = int(round(time.value)) _append_new_sub_element( - tc_element, "frame", text="{:.0f}".format(frame_number) + tc_element, "frame", text=f"{frame_number:.0f}" ) drop_frame = (";" in tc_string) @@ -1413,8 +1376,8 @@ def _build_item_timings( source_end = (item.source_range.end_time_exclusive() - timecode) source_end = source_end.rescaled_to(item_rate) - start = '{:.0f}'.format(timeline_range.start_time.value) - end = '{:.0f}'.format(timeline_range.end_time_exclusive().value) + start = f'{timeline_range.start_time.value:.0f}' + end = f'{timeline_range.end_time_exclusive().value:.0f}' item_e.append(_build_rate(item_rate)) @@ -1427,19 +1390,19 @@ def _build_item_timings( _append_new_sub_element( item_e, 'duration', - text='{:.0f}'.format(item.source_range.duration.value) + text=f'{item.source_range.duration.value:.0f}' ) _append_new_sub_element(item_e, 'start', text=start) _append_new_sub_element(item_e, 'end', text=end) _append_new_sub_element( item_e, 'in', - text='{:.0f}'.format(source_start.value) + text=f'{source_start.value:.0f}' ) _append_new_sub_element( item_e, 'out', - text='{:.0f}'.format(source_end.value) + text=f'{source_end.value:.0f}' ) @@ -1466,7 +1429,7 @@ def _build_empty_file(media_ref, parent_range, br_map): _append_new_sub_element( file_e, 'duration', - text='{:.0f}'.format(duration.value), + text=f'{duration.value:.0f}', ) # timecode @@ -1519,7 +1482,7 @@ def _build_file(media_reference, br_map): file_e.append(_build_rate(available_range.start_time.rate)) _append_new_sub_element( file_e, 'duration', - text='{:.0f}'.format(available_range.duration.value) + text=f'{available_range.duration.value:.0f}' ) # timecode @@ -1563,12 +1526,12 @@ def _build_transition_item( _append_new_sub_element( transition_e, 'start', - text='{:.0f}'.format(timeline_range.start_time.value) + text=f'{timeline_range.start_time.value:.0f}' ) _append_new_sub_element( transition_e, 'end', - text='{:.0f}'.format(timeline_range.end_time_exclusive().value) + text=f'{timeline_range.end_time_exclusive().value:.0f}' ) # Only add an alignment if it didn't already come in from the metadata dict @@ -1867,9 +1830,14 @@ def _build_marker(marker): _append_new_sub_element(marker_e, 'name', text=marker.name) _append_new_sub_element( marker_e, 'in', - text='{:.0f}'.format(marked_range.start_time.value) + text=f'{marked_range.start_time.value:.0f}' + ) + _append_new_sub_element( + marker_e, 'out', + text='{:.0f}'.format( + marked_range.start_time.value + marked_range.duration.value + ) ) - _append_new_sub_element(marker_e, 'out', text='-1') return marker_e @@ -1947,7 +1915,7 @@ def _add_stack_elements_to_sequence(stack, sequence_e, timeline_range, br_map): _append_new_sub_element(sequence_e, 'name', text=stack.name) _append_new_sub_element( sequence_e, 'duration', - text='{:.0f}'.format(timeline_range.duration.value) + text=f'{timeline_range.duration.value:.0f}' ) sequence_e.append(_build_rate(timeline_range.start_time.rate)) track_rate = timeline_range.start_time.rate @@ -1956,6 +1924,10 @@ def _add_stack_elements_to_sequence(stack, sequence_e, timeline_range, br_map): video_e = _get_or_create_subelement(media_e, 'video') audio_e = _get_or_create_subelement(media_e, 'audio') + # This is a fix for Davinci Resolve. After the "video" tag, it expects + # a tag, even if empty. See issue 839 + _get_or_create_subelement(video_e, "format") + # XXX: Due to the way that backreferences are created later on, the XML # is assumed to have its video tracks serialized before its audio # tracks. Because the order that they are added to the media is diff --git a/src/py-opentimelineio/opentimelineio/adapters/file_bundle_utils.py b/src/py-opentimelineio/opentimelineio/adapters/file_bundle_utils.py index 7888f8ec6..3cccff128 100644 --- a/src/py-opentimelineio/opentimelineio/adapters/file_bundle_utils.py +++ b/src/py-opentimelineio/opentimelineio/adapters/file_bundle_utils.py @@ -1,26 +1,5 @@ -# +# SPDX-License-Identifier: Apache-2.0 # Copyright Contributors to the OpenTimelineIO project -# -# Licensed under the Apache License, Version 2.0 (the "Apache License") -# with the following modification; you may not use this file except in -# compliance with the Apache License and the following modification to it: -# Section 6. Trademarks. is deleted and replaced with: -# -# 6. Trademarks. This License does not grant permission to use the trade -# names, trademarks, service marks, or product names of the Licensor -# and its affiliates, except as required to comply with Section 4(c) of -# the License and to reproduce the content of the NOTICE file. -# -# You may obtain a copy of the Apache License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the Apache License with the above modification is -# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -# KIND, either express or implied. See the Apache License for the specific -# language governing permissions and limitations under the Apache License. -# """Common utilities used by the file bundle adapters (otiod and otioz).""" @@ -34,13 +13,7 @@ url_utils, ) - -try: - # Python 2.7 - import urlparse -except ImportError: - # Python 3 - import urllib.parse as urlparse +import urllib.parse as urlparse # versioning @@ -122,11 +95,11 @@ def _prepped_otio_for_bundle_and_manifest( invalid_files = set() # result_otio is manipulated in place - for cl in result_otio.each_clip(): + for cl in result_otio.find_clips(): if media_policy == MediaReferencePolicy.AllMissing: cl.media_reference = reference_cloned_and_missing( cl.media_reference, - "{} specified as the MediaReferencePolicy".format(media_policy) + f"{media_policy} specified as the MediaReferencePolicy" ) continue diff --git a/src/py-opentimelineio/opentimelineio/adapters/otio_json.py b/src/py-opentimelineio/opentimelineio/adapters/otio_json.py index a1bbe01bf..4bcffdb08 100644 --- a/src/py-opentimelineio/opentimelineio/adapters/otio_json.py +++ b/src/py-opentimelineio/opentimelineio/adapters/otio_json.py @@ -1,35 +1,17 @@ -# +# SPDX-License-Identifier: Apache-2.0 # Copyright Contributors to the OpenTimelineIO project -# -# Licensed under the Apache License, Version 2.0 (the "Apache License") -# with the following modification; you may not use this file except in -# compliance with the Apache License and the following modification to it: -# Section 6. Trademarks. is deleted and replaced with: -# -# 6. Trademarks. This License does not grant permission to use the trade -# names, trademarks, service marks, or product names of the Licensor -# and its affiliates, except as required to comply with Section 4(c) of -# the License and to reproduce the content of the NOTICE file. -# -# You may obtain a copy of the Apache License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the Apache License with the above modification is -# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -# KIND, either express or implied. See the Apache License for the specific -# language governing permissions and limitations under the Apache License. -# - -"""This adapter lets you read and write native .otio files""" + +"""Adapter for reading and writing native .otio json files.""" from .. import ( - core + core, + versioning, + exceptions ) +import os -# @TODO: Implement out of process plugins that hand around JSON +_DEFAULT_VERSION_ENVVAR = "OTIO_DEFAULT_TARGET_VERSION_FAMILY_LABEL" def read_from_file(filepath): @@ -58,7 +40,39 @@ def read_from_string(input_str): return core.deserialize_json_from_string(input_str) -def write_to_string(input_otio, indent=4): +def _fetch_downgrade_map_from_env(): + version_envvar = os.environ[_DEFAULT_VERSION_ENVVAR] + + try: + family, label = version_envvar.split(":") + except ValueError: + raise exceptions.InvalidEnvironmentVariableError( + "Environment variable '{}' is incorrectly formatted with '{}'." + "Variable must be formatted as 'FAMILY:LABEL'".format( + _DEFAULT_VERSION_ENVVAR, + version_envvar, + ) + ) + + try: + # technically fetch_map returns an AnyDictionary, but the pybind11 + # code wrapping the call to the serializer expects a python + # dictionary. This turns it back into a normal dictionary. + return dict(versioning.fetch_map(family, label)) + except KeyError: + raise exceptions.InvalidEnvironmentVariableError( + "Environment variable '{}' is requesting family '{}' and label" + " '{}', however this combination does not exist in the " + "currently loaded manifests. Full version map: {}".format( + _DEFAULT_VERSION_ENVVAR, + family, + label, + versioning.full_map() + ) + ) + + +def write_to_string(input_otio, target_schema_versions=None, indent=4): """ Serializes an OpenTimelineIO object into a string @@ -67,13 +81,39 @@ def write_to_string(input_otio, indent=4): indent (int): number of spaces for each json indentation level. Use\ -1 for no indentation or newlines. + If target_schema_versions is None and the environment variable + "OTIO_DEFAULT_TARGET_VERSION_FAMILY_LABEL" is set, will read a map out of + that for downgrade target. The variable should be of the form + FAMILY:LABEL, for example "MYSTUDIO:JUNE2022". + Returns: str: A json serialized string representation - """ - return core.serialize_json_to_string(input_otio, indent) + Raises: + otio.exceptions.InvalidEnvironmentVariableError: if there is a problem + with the default environment variable + "OTIO_DEFAULT_TARGET_VERSION_FAMILY_LABEL". + """ -def write_to_file(input_otio, filepath, indent=4): + if ( + target_schema_versions is None + and _DEFAULT_VERSION_ENVVAR in os.environ + ): + target_schema_versions = _fetch_downgrade_map_from_env() + + return core.serialize_json_to_string( + input_otio, + target_schema_versions, + indent + ) + + +def write_to_file( + input_otio, + filepath, + target_schema_versions=None, + indent=4 +): """ Serializes an OpenTimelineIO object into a file @@ -84,10 +124,30 @@ def write_to_file(input_otio, filepath, indent=4): indent (int): number of spaces for each json indentation level.\ Use -1 for no indentation or newlines. + If target_schema_versions is None and the environment variable + "OTIO_DEFAULT_TARGET_VERSION_FAMILY_LABEL" is set, will read a map out of + that for downgrade target. The variable should be of the form + FAMILY:LABEL, for example "MYSTUDIO:JUNE2022". + Returns: bool: Write success Raises: ValueError: on write error + otio.exceptions.InvalidEnvironmentVariableError: if there is a problem + with the default environment variable + "OTIO_DEFAULT_TARGET_VERSION_FAMILY_LABEL". """ - return core.serialize_json_to_file(input_otio, filepath, indent) + + if ( + target_schema_versions is None + and _DEFAULT_VERSION_ENVVAR in os.environ + ): + target_schema_versions = _fetch_downgrade_map_from_env() + + return core.serialize_json_to_file( + input_otio, + filepath, + target_schema_versions, + indent + ) diff --git a/src/py-opentimelineio/opentimelineio/adapters/otiod.py b/src/py-opentimelineio/opentimelineio/adapters/otiod.py index 277da1106..77028dcd8 100644 --- a/src/py-opentimelineio/opentimelineio/adapters/otiod.py +++ b/src/py-opentimelineio/opentimelineio/adapters/otiod.py @@ -1,26 +1,5 @@ -# +# SPDX-License-Identifier: Apache-2.0 # Copyright Contributors to the OpenTimelineIO project -# -# Licensed under the Apache License, Version 2.0 (the "Apache License") -# with the following modification; you may not use this file except in -# compliance with the Apache License and the following modification to it: -# Section 6. Trademarks. is deleted and replaced with: -# -# 6. Trademarks. This License does not grant permission to use the trade -# names, trademarks, service marks, or product names of the Licensor -# and its affiliates, except as required to comply with Section 4(c) of -# the License and to reproduce the content of the NOTICE file. -# -# You may obtain a copy of the Apache License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the Apache License with the above modification is -# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -# KIND, either express or implied. See the Apache License for the specific -# language governing permissions and limitations under the Apache License. -# """OTIOD adapter - bundles otio files linked to local media in a directory @@ -43,18 +22,8 @@ url_utils, ) -try: - import pathlib -except ImportError: - # python2 - import pathlib2 as pathlib - -try: - # Python 2.7 - import urlparse -except ImportError: - # Python 3 - import urllib.parse as urlparse +import pathlib +import urllib.parse as urlparse def read_from_file(filepath, absolute_media_reference_paths=False): @@ -65,7 +34,7 @@ def read_from_file(filepath, absolute_media_reference_paths=False): if not absolute_media_reference_paths: return result - for cl in result.each_clip(): + for cl in result.find_clips(): try: source_fpath = cl.media_reference.target_url except AttributeError: @@ -90,7 +59,7 @@ def write_to_file( if os.path.exists(filepath): raise exceptions.OTIOError( - "'{}' exists, will not overwrite.".format(filepath) + f"'{filepath}' exists, will not overwrite." ) if not os.path.exists(os.path.dirname(filepath)): diff --git a/src/py-opentimelineio/opentimelineio/adapters/otioz.py b/src/py-opentimelineio/opentimelineio/adapters/otioz.py index 4422cb9fe..89d950903 100644 --- a/src/py-opentimelineio/opentimelineio/adapters/otioz.py +++ b/src/py-opentimelineio/opentimelineio/adapters/otioz.py @@ -1,26 +1,5 @@ -# +# SPDX-License-Identifier: Apache-2.0 # Copyright Contributors to the OpenTimelineIO project -# -# Licensed under the Apache License, Version 2.0 (the "Apache License") -# with the following modification; you may not use this file except in -# compliance with the Apache License and the following modification to it: -# Section 6. Trademarks. is deleted and replaced with: -# -# 6. Trademarks. This License does not grant permission to use the trade -# names, trademarks, service marks, or product names of the Licensor -# and its affiliates, except as required to comply with Section 4(c) of -# the License and to reproduce the content of the NOTICE file. -# -# You may obtain a copy of the Apache License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the Apache License with the above modification is -# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -# KIND, either express or implied. See the Apache License for the specific -# language governing permissions and limitations under the Apache License. -# """OTIOZ adapter - bundles otio files linked to local media @@ -50,16 +29,12 @@ otio_json ) -try: - import pathlib -except ImportError: - # python2 - import pathlib2 as pathlib +import pathlib def read_from_file(filepath, extract_to_directory=None): if not zipfile.is_zipfile(filepath): - raise exceptions.OTIOError("Not a zipfile: {}".format(filepath)) + raise exceptions.OTIOError(f"Not a zipfile: {filepath}") if extract_to_directory: output_media_directory = os.path.join( @@ -101,7 +76,7 @@ def write_to_file( if os.path.exists(filepath): raise exceptions.OTIOError( - "'{}' exists, will not overwrite.".format(filepath) + f"'{filepath}' exists, will not overwrite." ) # general algorithm for the file bundle adapters: @@ -150,6 +125,17 @@ def write_to_file( target.writestr( utils.BUNDLE_VERSION_FILE, utils.BUNDLE_VERSION, + # XXX: OTIOZ was introduced when python 2.7 was still a supported + # platform. The newer algorithms, like BZIP2 and LZMA, are not + # available in python2, so it uses the zlib based + # ZIP_DEFLATED. Now that OTIO is Python3+, this could switch + # to using BZIP2 or LZMA instead... with the caveat that this + # would make OTIOZ files incompatible with python 2 based OTIO + # installs. + # + # For example, if we used ZIP_LZMA, then otio release v0.15 + # would still be able to open these files as long as the + # python interpreter was version 3+. compress_type=zipfile.ZIP_DEFLATED ) @@ -157,7 +143,7 @@ def write_to_file( target.writestr( utils.BUNDLE_PLAYLIST_PATH, otio_str, - # Python 3 use ZIP_LZMA + # XXX: See comment above about ZIP_DEFLATED vs other algorithms compress_type=zipfile.ZIP_DEFLATED ) diff --git a/src/py-opentimelineio/opentimelineio/adapters/svg.py b/src/py-opentimelineio/opentimelineio/adapters/svg.py index 28841378e..54cc94c2e 100644 --- a/src/py-opentimelineio/opentimelineio/adapters/svg.py +++ b/src/py-opentimelineio/opentimelineio/adapters/svg.py @@ -1,26 +1,5 @@ -# +# SPDX-License-Identifier: Apache-2.0 # Copyright Contributors to the OpenTimelineIO project -# -# Licensed under the Apache License, Version 2.0 (the "Apache License") -# with the following modification; you may not use this file except in -# compliance with the Apache License and the following modification to it: -# Section 6. Trademarks. is deleted and replaced with: -# -# 6. Trademarks. This License does not grant permission to use the trade -# names, trademarks, service marks, or product names of the Licensor -# and its affiliates, except as required to comply with Section 4(c) of -# the License and to reproduce the content of the NOTICE file. -# -# You may obtain a copy of the Apache License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the Apache License with the above modification is -# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -# KIND, either express or implied. See the Apache License for the specific -# language governing permissions and limitations under the Apache License. -# """OTIO to SVG Adapter Points in calculations are y-up. @@ -33,8 +12,7 @@ # python import math -from random import seed -from random import random +import random random_colors_used = [] @@ -69,7 +47,7 @@ def __generate_new_color(): @staticmethod def __get_random_color(): - return Color(random(), random(), random(), 1.0) + return Color(random.random(), random.random(), random.random(), 1.0) @staticmethod def __color_distance(c1, c2): @@ -92,8 +70,11 @@ def a(self): return self.value[3] def svg_color(self): - return 'rgb({:.8f},{:.8f},{:.8f})'.format(self.r * 255.0, self.g * 255.0, - self.b * 255.0) + return 'rgb({:.8f},{:.8f},{:.8f})'.format( + self.r * 255.0, + self.g * 255.0, + self.b * 255.0 + ) COLORS = { @@ -126,10 +107,10 @@ def __init__(self, x, y): self.y = y def svg_point_string(self): - return "{:.8f},{:.8f}".format(self.x, self.y) + return f"{self.x:.8f},{self.y:.8f}" -class Rect(object): +class Rect: origin = Point(0, 0) width = 0.0 height = 0.0 @@ -191,9 +172,16 @@ def convert_rect_to_svg_coordinates(rect, image_height): class SVGWriter: - def __init__(self, image_width=2406.0, image_height=1054.0, image_margin=20.0, - arrow_margin=10.0, arrow_label_margin=5.0, font_size=15.0, - font_family='sans-serif'): + def __init__( + self, + image_width=2406.0, + image_height=1054.0, + image_margin=20.0, + arrow_margin=10.0, + arrow_label_margin=5.0, + font_size=15.0, + font_family='sans-serif' + ): self.image_width = image_width self.image_height = image_height self.image_margin = image_margin @@ -215,14 +203,16 @@ def __init__(self, image_width=2406.0, image_height=1054.0, image_margin=20.0, self.x_origin = 0 self.clip_rect_height = 0 self.vertical_drawing_index = -1 - self.svg_elem = Element("svg", - { - "height": "{:.8f}".format(self.image_height), - "width": "{:.8f}".format(self.image_width), - "version": "4.0", - "xmlns": "http://www.w3.org/2000/svg", - "xmlns:xlink": "http://www.w3.org/1999/xlink", - }) + self.svg_elem = Element( + "svg", + { + "height": f"{self.image_height:.8f}", + "width": f"{self.image_width:.8f}", + "version": "4.0", + "xmlns": "http://www.w3.org/2000/svg", + "xmlns:xlink": "http://www.w3.org/1999/xlink", + } + ) # white background SubElement(self.svg_elem, "rect", @@ -234,16 +224,21 @@ def __init__(self, image_width=2406.0, image_height=1054.0, image_margin=20.0, def draw_rect(self, rect, stroke_width=2.0, stroke_color=COLORS['black']): svg_rect = convert_rect_to_svg_coordinates(rect, self.image_height) - SubElement(self.svg_elem, "rect", - { - "x": "{:.8f}".format(svg_rect.origin.x), - "y": "{:.8f}".format(svg_rect.origin.y), - "width": "{:.8f}".format(svg_rect.width), - "height": "{:.8f}".format(svg_rect.height), - "style": "fill:rgb(255,255,255);stroke-width:{:.8f};" - "stroke:{};opacity:1;fill-opacity:0;".format( - stroke_width, - stroke_color.svg_color())}) + SubElement( + self.svg_elem, + "rect", + { + "x": f"{svg_rect.origin.x:.8f}", + "y": f"{svg_rect.origin.y:.8f}", + "width": f"{svg_rect.width:.8f}", + "height": f"{svg_rect.height:.8f}", + "style": "fill:rgb(255,255,255);stroke-width:{:.8f};" + "stroke:{};opacity:1;fill-opacity:0;".format( + stroke_width, + stroke_color.svg_color() + ) + } + ) def draw_labeled_rect(self, rect, stroke_width=2.0, stroke_color=COLORS['black'], @@ -257,26 +252,30 @@ def draw_labeled_rect(self, rect, stroke_width=2.0, svg_rect.origin.x, svg_rect.origin.y) }) - SubElement(g_elem, "rect", - { - "width": "{:.8f}".format(svg_rect.width), - "height": "{:.8f}".format(svg_rect.height), - "style": "fill:{};stroke-width:{:.8f};" - "stroke:{};opacity:1;".format( - fill_color.svg_color(), - stroke_width, - stroke_color.svg_color()) - }) + SubElement( + g_elem, + "rect", + { + "width": f"{svg_rect.width:.8f}", + "height": f"{svg_rect.height:.8f}", + "style": "fill:{};stroke-width:{:.8f};" + "stroke:{};opacity:1;".format( + fill_color.svg_color(), + stroke_width, + stroke_color.svg_color() + ) + } + ) sub_svg_elem = SubElement(g_elem, "svg", { - "width": "{:.8f}".format(svg_rect.width), - "height": "{:.8f}".format(svg_rect.height) + "width": f"{svg_rect.width:.8f}", + "height": f"{svg_rect.height:.8f}" }) text_elem = SubElement(sub_svg_elem, "text", { "x": "50%", "y": "50%", - "font-size": "{:.8f}".format(label_size), + "font-size": f"{label_size:.8f}", "font-family": self.font_family, "style": "stroke:{};stroke-width:{:.8f};" @@ -289,23 +288,32 @@ def draw_labeled_rect(self, rect, stroke_width=2.0, "text-anchor": "middle"}) text_elem.text = label - def draw_dashed_rect(self, rect, stroke_width=2.0, stroke_color=COLORS['black'], - fill_color=COLORS['white']): + def draw_dashed_rect( + self, + rect, + stroke_width=2.0, + stroke_color=COLORS['black'], + fill_color=COLORS['white'] + ): svg_rect = convert_rect_to_svg_coordinates(rect, self.image_height) - SubElement(self.svg_elem, "rect", - { - "x": "{:.8f}".format(svg_rect.origin.x), - "y": "{:.8f}".format(svg_rect.origin.y), - "width": "{:.8f}".format(svg_rect.width), - "height": "{:.8f}".format(svg_rect.height), - "stroke-dasharray": "5", - "style": "fill:{};stroke-width:{:.8f};stroke:{};" - "opacity:1;fill-opacity:{:.8f}".format( - fill_color.svg_color(), - stroke_width, - stroke_color.svg_color(), - fill_color.a) - }) + SubElement( + self.svg_elem, + "rect", + { + "x": f"{svg_rect.origin.x:.8f}", + "y": f"{svg_rect.origin.y:.8f}", + "width": f"{svg_rect.width:.8f}", + "height": f"{svg_rect.height:.8f}", + "stroke-dasharray": "5", + "style": "fill:{};stroke-width:{:.8f};stroke:{};" + "opacity:1;fill-opacity:{:.8f}".format( + fill_color.svg_color(), + stroke_width, + stroke_color.svg_color(), + fill_color.a + ) + } + ) def draw_labeled_dashed_rect_with_border(self, rect, stroke_width=2.0, fill_color=COLORS['white'], @@ -318,71 +326,86 @@ def draw_labeled_dashed_rect_with_border(self, rect, stroke_width=2.0, "transform": "translate({:.8f},{:.8f})".format( svg_rect.origin.x, svg_rect.origin.y) }) - SubElement(g_elem, "rect", - { - "width": "{:.8f}".format(svg_rect.width), - "height": "{:.8f}".format(svg_rect.height), - "stroke-dasharray": "5", - "style": "fill:{};stroke-width:{:.8f};" - "stroke:{};opacity:{:.8f};".format( - fill_color.svg_color(), - stroke_width, - border_color.svg_color(), - fill_color.a) - }) + SubElement( + g_elem, + "rect", + { + "width": f"{svg_rect.width:.8f}", + "height": f"{svg_rect.height:.8f}", + "stroke-dasharray": "5", + "style": "fill:{};stroke-width:{:.8f};" + "stroke:{};opacity:{:.8f};".format( + fill_color.svg_color(), + stroke_width, + border_color.svg_color(), + fill_color.a + ) + } + ) sub_svg_elem = SubElement(g_elem, "svg", { - "width": "{:.8f}".format(svg_rect.width), - "height": "{:.8f}".format(svg_rect.height) + "width": f"{svg_rect.width:.8f}", + "height": f"{svg_rect.height:.8f}" }) - text_elem = SubElement(sub_svg_elem, "text", - { - "x": "50%", - "y": "50%", - "font-size": "{:.8f}".format(label_size), - "font-family": self.font_family, - "style": "stroke:{};stroke-width:{:.8f};" - "fill:{};opacity:{:.8f};".format( - COLORS['black'].svg_color(), - stroke_width / 4.0, - COLORS['black'].svg_color(), - COLORS['black'].a), - "alignment-baseline": "middle", - "text-anchor": "middle" - }) + text_elem = SubElement( + sub_svg_elem, + "text", + { + "x": "50%", + "y": "50%", + "font-size": f"{label_size:.8f}", + "font-family": self.font_family, + "style": "stroke:{};stroke-width:{:.8f};" + "fill:{};opacity:{:.8f};".format( + COLORS['black'].svg_color(), + stroke_width / 4.0, + COLORS['black'].svg_color(), + COLORS['black'].a), + "alignment-baseline": "middle", + "text-anchor": "middle" + } + ) text_elem.text = label def draw_solid_rect(self, rect, fill_color=COLORS['white']): svg_rect = convert_rect_to_svg_coordinates(rect, self.image_height) - SubElement(self.svg_elem, "rect", - { - "x": "{:.8f}".format(svg_rect.origin.x), - "y": "{:.8f}".format(svg_rect.origin.y), - "width": "{:.8f}".format(svg_rect.width), - "height": "{:.8f}".format(svg_rect.height), - "style": "fill:{};stroke-width:0;" - "stroke:rgb(0,0,0);opacity:{:.8f};".format( - fill_color.svg_color(), - fill_color.a) - }) + SubElement( + self.svg_elem, + "rect", + { + "x": f"{svg_rect.origin.x:.8f}", + "y": f"{svg_rect.origin.y:.8f}", + "width": f"{svg_rect.width:.8f}", + "height": f"{svg_rect.height:.8f}", + "style": "fill:{};stroke-width:0;" + "stroke:rgb(0,0,0);opacity:{:.8f};".format( + fill_color.svg_color(), + fill_color.a + ) + } + ) def draw_solid_rect_with_border(self, rect, stroke_width=2.0, fill_color=COLORS['white'], border_color=COLORS['black']): svg_rect = convert_rect_to_svg_coordinates(rect, self.image_height) - SubElement(self.svg_elem, "rect", - { - "x": "{:.8f}".format(svg_rect.origin.x), - "y": "{:.8f}".format(svg_rect.origin.y), - "width": "{:.8f}".format(svg_rect.width), - "height": "{:.8f}".format(svg_rect.height), - "style": "fill:{};stroke-width:{:.8f};" - "stroke:{};opacity:{:.8f};".format( - fill_color.svg_color(), - stroke_width, - border_color.svg_color(), - fill_color.a) - }) + SubElement( + self.svg_elem, + "rect", + { + "x": f"{svg_rect.origin.x:.8f}", + "y": f"{svg_rect.origin.y:.8f}", + "width": f"{svg_rect.width:.8f}", + "height": f"{svg_rect.height:.8f}", + "style": "fill:{};stroke-width:{:.8f};" + "stroke:{};opacity:{:.8f};".format( + fill_color.svg_color(), + stroke_width, + border_color.svg_color(), + fill_color.a + ) + } + ) def draw_labeled_solid_rect_with_border(self, rect, stroke_width=2.0, fill_color=COLORS['white'], @@ -395,41 +418,56 @@ def draw_labeled_solid_rect_with_border(self, rect, stroke_width=2.0, "transform": "translate({:.8f},{:.8f})".format( svg_rect.origin.x, svg_rect.origin.y) }) - SubElement(g_elem, "rect", - { - "width": "{:.8f}".format(svg_rect.width), - "height": "{:.8f}".format(svg_rect.height), - "style": "fill:{};stroke-width:{:.8f};" - "stroke:{};opacity:{:.8f};".format( - fill_color.svg_color(), - stroke_width, - border_color.svg_color(), - fill_color.a) - }) - sub_svg_elem = SubElement(g_elem, "svg", - { - "width": "{:.8f}".format(svg_rect.width), - "height": "{:.8f}".format(svg_rect.height) - }) - text_elem = SubElement(sub_svg_elem, "text", - { - "x": "50%", - "y": "50%", - "font-size": "{:.8f}".format(label_size), - "font-family": self.font_family, - "style": "stroke:{};stroke-width:{:.8f};" - "fill:{};opacity:{:.8f};".format( - COLORS['black'].svg_color(), - stroke_width / 4.0, - COLORS['black'].svg_color(), - COLORS['black'].a), - "alignment-baseline": "middle", - "text-anchor": "middle"}) + SubElement( + g_elem, + "rect", + { + "width": f"{svg_rect.width:.8f}", + "height": f"{svg_rect.height:.8f}", + "style": "fill:{};stroke-width:{:.8f};" + "stroke:{};opacity:{:.8f};".format( + fill_color.svg_color(), + stroke_width, + border_color.svg_color(), + fill_color.a + ) + } + ) + sub_svg_elem = SubElement( + g_elem, + "svg", + { + "width": f"{svg_rect.width:.8f}", + "height": f"{svg_rect.height:.8f}" + } + ) + text_elem = SubElement( + sub_svg_elem, + "text", + { + "x": "50%", + "y": "50%", + "font-size": f"{label_size:.8f}", + "font-family": self.font_family, + "style": "stroke:{};stroke-width:{:.8f};" + "fill:{};opacity:{:.8f};".format( + COLORS['black'].svg_color(), + stroke_width / 4.0, + COLORS['black'].svg_color(), + COLORS['black'].a + ), + "alignment-baseline": "middle", + "text-anchor": "middle" + } + ) text_elem.text = label def draw_line(self, start_point, end_point, stroke_width, stroke_color=COLORS['black'], is_dashed=False): - point1 = convert_point_to_svg_coordinates(start_point, self.image_height) + point1 = convert_point_to_svg_coordinates( + start_point, + self.image_height + ) point2 = convert_point_to_svg_coordinates(end_point, self.image_height) style_str = "stroke-width:{:.8f};stroke:{}" \ ";opacity:{:.8f};" \ @@ -440,16 +478,19 @@ def draw_line(self, start_point, end_point, stroke_width, style_str = style_str + "stroke-dasharray:4 1" SubElement(self.svg_elem, "line", { - "x1": "{:.8f}".format(point1.x), - "y1": "{:.8f}".format(point1.y), - "x2": "{:.8f}".format(point2.x), - "y2": "{:.8f}".format(point2.y), + "x1": f"{point1.x:.8f}", + "y1": f"{point1.y:.8f}", + "x2": f"{point2.x:.8f}", + "y2": f"{point2.y:.8f}", "style": style_str }) def draw_arrow(self, start_point, end_point, stroke_width, stroke_color=COLORS['black']): - point1 = convert_point_to_svg_coordinates(start_point, self.image_height) + point1 = convert_point_to_svg_coordinates( + start_point, + self.image_height + ) point2 = convert_point_to_svg_coordinates(end_point, self.image_height) direction = Point(point2.x - point1.x, point2.y - point1.y) direction_magnitude = math.sqrt(direction.x * direction.x + @@ -457,65 +498,91 @@ def draw_arrow(self, start_point, end_point, stroke_width, inv_magnitude = 1.0 / direction_magnitude arrowhead_length = 9.0 arrowhead_half_width = arrowhead_length * 0.5 - direction = Point(direction.x * inv_magnitude, direction.y * inv_magnitude) + direction = Point( + direction.x * inv_magnitude, + direction.y * inv_magnitude + ) point2 = Point(point2.x - arrowhead_length * direction.x, point2.y - arrowhead_length * direction.y) triangle_tip = Point(point2.x + arrowhead_length * direction.x, point2.y + arrowhead_length * direction.y) perpendicular_dir = Point(-direction.y, direction.x) - triangle_pt_1 = Point(point2.x + arrowhead_half_width * perpendicular_dir.x, - point2.y + arrowhead_half_width * perpendicular_dir.y) - triangle_pt_2 = Point(point2.x - arrowhead_half_width * perpendicular_dir.x, - point2.y - arrowhead_half_width * perpendicular_dir.y) - SubElement(self.svg_elem, "line", - { - "x1": "{:.8f}".format(point1.x), - "y1": "{:.8f}".format(point1.y), - "x2": "{:.8f}".format(point2.x), - "y2": "{:.8f}".format(point2.y), - "style": "stroke-width:{:.8f};stroke:{};opacity:{:.8f};" - "stroke-linecap:butt;".format( - stroke_width, - stroke_color.svg_color(), - stroke_color.a) - }) - SubElement(self.svg_elem, "polygon", - { - "points": " ".join(p.svg_point_string() for p in - [triangle_tip, triangle_pt_1, triangle_pt_2]), - "style": "fill:{};".format(stroke_color.svg_color()) - }) + triangle_pt_1 = Point( + point2.x + arrowhead_half_width * perpendicular_dir.x, + point2.y + arrowhead_half_width * perpendicular_dir.y + ) + triangle_pt_2 = Point( + point2.x - arrowhead_half_width * perpendicular_dir.x, + point2.y - arrowhead_half_width * perpendicular_dir.y + ) + SubElement( + self.svg_elem, + "line", + { + "x1": f"{point1.x:.8f}", + "y1": f"{point1.y:.8f}", + "x2": f"{point2.x:.8f}", + "y2": f"{point2.y:.8f}", + "style": "stroke-width:{:.8f};stroke:{};opacity:{:.8f};" + "stroke-linecap:butt;".format( + stroke_width, + stroke_color.svg_color(), + stroke_color.a + ) + } + ) + SubElement( + self.svg_elem, + "polygon", + { + "points": " ".join( + p.svg_point_string() + for p in [ + triangle_tip, + triangle_pt_1, + triangle_pt_2 + ] + ), + "style": f"fill:{stroke_color.svg_color()};" + } + ) def draw_text(self, text, location, text_size, color=COLORS['black'], stroke_width=1.0): - location_svg = convert_point_to_svg_coordinates(location, self.image_height) - text_elem = SubElement(self.svg_elem, "text", - { - "x": "{:.8f}".format(location_svg.x), - "y": "{:.8f}".format(location_svg.y), - "font-size": "{:.8f}".format(text_size), - "font-family": self.font_family, - "style": "stroke:{};stroke-width:{:.8f};" - "fill:{};opacity:{:.8f};".format( - color.svg_color(), - stroke_width / 4.0, - color.svg_color(), - color.a) - }) + location_svg = convert_point_to_svg_coordinates( + location, + self.image_height + ) + text_elem = SubElement( + self.svg_elem, + "text", + { + "x": f"{location_svg.x:.8f}", + "y": f"{location_svg.y:.8f}", + "font-size": f"{text_size:.8f}", + "font-family": self.font_family, + "style": "stroke:{};stroke-width:{:.8f};" + "fill:{};opacity:{:.8f};".format( + color.svg_color(), + stroke_width / 4.0, + color.svg_color(), + color.a + ) + } + ) text_elem.text = text def get_image(self): - # Python 3 produces a bytestring with the tostring() method, whereas Python 2 - # gives an str object. The try-except block below checks for this case. - xmlstr = tostring(self.svg_elem, encoding='utf-8', method='xml') - try: - xmlstr = xmlstr.decode("utf8") - except UnicodeDecodeError: - pass + xmlstr = tostring( + self.svg_elem, + encoding='utf-8', + method='xml' + ).decode('utf8') + return minidom.parseString(xmlstr).toprettyxml(indent=' ') -class ClipData(object): +class ClipData: def __init__(self, src_start=0.0, src_end=0.0, avlbl_start=0.0, avlbl_end=0.0, avlbl_duration=0.0, @@ -566,7 +633,9 @@ def _draw_timeline(timeline, svg_writer, extra_data=()): current_transition = item current_track_clips_data[-1].transition_end = item continue - avlbl_start = track_duration - item.trimmed_range().start_time.value + avlbl_start = ( + track_duration - item.trimmed_range().start_time.value + ) if isinstance(item, otio.schema.Clip): avlbl_start += item.available_range().start_time.value min_time = min(min_time, avlbl_start) @@ -577,10 +646,13 @@ def _draw_timeline(timeline, svg_writer, extra_data=()): trim_start = item.trimmed_range().start_time.value trim_duration = item.trimmed_range().duration.value if isinstance(item, otio.schema.Clip): - avlbl_end = (item.available_range().start_time.value + - item.available_range().duration.value - - item.trimmed_range().start_time.value - - item.trimmed_range().duration.value + track_duration - 1) + avlbl_end = ( + item.available_range().start_time.value + + item.available_range().duration.value + - item.trimmed_range().start_time.value + - item.trimmed_range().duration.value + + track_duration - 1 + ) clip_count += 1 avlbl_duration = item.available_range().duration.value clip_data = ClipData(src_start, src_end, avlbl_start, @@ -605,7 +677,9 @@ def _draw_timeline(timeline, svg_writer, extra_data=()): svg_writer.global_min_time = min(svg_writer.global_min_time, min_time) svg_writer.all_clips_data.append(current_track_clips_data) svg_writer.tracks_duration.append(track_duration) - svg_writer.track_transition_available.append(current_track_has_transition) + svg_writer.track_transition_available.append( + current_track_has_transition + ) if current_track_has_transition: transition_track_count += 1 # store track-wise clip count to draw arrows from stack to tracks @@ -617,8 +691,10 @@ def _draw_timeline(timeline, svg_writer, extra_data=()): len(svg_writer.trackwise_clip_count) - 1]) # The scale in x direction is calculated considering margins on the # left and right side if the image - svg_writer.scale_x = (svg_writer.image_width - (2.0 * svg_writer.image_margin)) / \ - (svg_writer.global_max_time - svg_writer.global_min_time + 1.0) + svg_writer.scale_x = ( + (svg_writer.image_width - (2.0 * svg_writer.image_margin)) + / (svg_writer.global_max_time - svg_writer.global_min_time + 1.0) + ) svg_writer.x_origin = ((-svg_writer.global_min_time) * svg_writer.scale_x + svg_writer.image_margin) track_count = len(svg_writer.tracks_duration) @@ -631,13 +707,17 @@ def _draw_timeline(timeline, svg_writer, extra_data=()): # components on that track # + 2.0 = timeline and stack rects # clip_count = we need to draw a rect for a media reference per clip - # transition_track_count = we need one more row per the number of tracks with - # transitions - # NumberOfRects * 2.0 - 1.0 = to account for "one rect space" between all the rects + # transition_track_count = we need one more row per the number of tracks + # with transitions + # NumberOfRects * 2.0 - 1.0 = to account for "one rect space" between all + # the rects total_image_margin_space = 2.0 * svg_writer.image_margin bottom_label_space = 2.0 * svg_writer.font_size - svg_total_draw_space = (svg_writer.image_height - total_image_margin_space - - bottom_label_space) + svg_total_draw_space = ( + svg_writer.image_height + - total_image_margin_space + - bottom_label_space + ) track_sequence_rect_count = track_count * 2.0 timeline_stack_rect_count = 2.0 rect_count = (track_sequence_rect_count + timeline_stack_rect_count + @@ -654,15 +734,25 @@ def _draw_timeline(timeline, svg_writer, extra_data=()): svg_writer.max_total_duration = max(svg_writer.tracks_duration) label_text_size = 0.4 * svg_writer.clip_rect_height svg_writer.draw_labeled_solid_rect_with_border( - Rect(timeline_origin, svg_writer.max_total_duration * svg_writer.scale_x, - svg_writer.clip_rect_height), label="Timeline", label_size=label_text_size) + Rect( + timeline_origin, + svg_writer.max_total_duration * svg_writer.scale_x, + svg_writer.clip_rect_height + ), + label="Timeline", + label_size=label_text_size + ) time_marker_height = 0.15 * svg_writer.clip_rect_height for i in range(1, int(svg_writer.max_total_duration)): start_pt = Point(svg_writer.x_origin + (i * svg_writer.scale_x), timeline_origin.y) end_pt = Point(start_pt.x, start_pt.y + time_marker_height) - svg_writer.draw_line(start_point=start_pt, end_point=end_pt, stroke_width=1.0, - stroke_color=COLORS['black']) + svg_writer.draw_line( + start_point=start_pt, + end_point=end_pt, + stroke_width=1.0, + stroke_color=COLORS['black'] + ) # Draw arrow from timeline to stack timeline_width = svg_writer.max_total_duration * svg_writer.scale_x arrow_start = Point(svg_writer.x_origin + timeline_width * 0.5, @@ -683,7 +773,11 @@ def _draw_timeline(timeline, svg_writer, extra_data=()): repr(float(round(timeline.global_start_time.value, 1)))) start_time_location = Point(timeline_origin.x + svg_writer.font_size, timeline_origin.y - svg_writer.font_size) - svg_writer.draw_text(start_time_text, start_time_location, svg_writer.font_size) + svg_writer.draw_text( + start_time_text, + start_time_location, + svg_writer.font_size + ) # Draw stack draw_item(timeline.tracks, svg_writer, @@ -707,14 +801,28 @@ def _draw_stack(stack, svg_writer, extra_data=()): label_size=stack_text_size) time_marker_height = 0.15 * svg_writer.clip_rect_height for i in range(1, int(svg_writer.max_total_duration)): - start_pt = Point(svg_writer.x_origin + (i * svg_writer.scale_x), stack_origin.y) + start_pt = Point( + svg_writer.x_origin + (i * svg_writer.scale_x), + stack_origin.y + ) end_pt = Point(start_pt.x, start_pt.y + time_marker_height) - svg_writer.draw_line(start_point=start_pt, end_point=end_pt, stroke_width=1.0, - stroke_color=COLORS['black']) + svg_writer.draw_line( + start_point=start_pt, + end_point=end_pt, + stroke_width=1.0, + stroke_color=COLORS['black'] + ) for i in range(0, len(svg_writer.tracks_duration)): - draw_item(stack[i], svg_writer, (stack_x_origin, svg_writer.tracks_duration[i], - svg_writer.all_clips_data[i], - svg_writer.track_transition_available[i])) + draw_item( + stack[i], + svg_writer, + ( + stack_x_origin, + svg_writer.tracks_duration[i], + svg_writer.all_clips_data[i], + svg_writer.track_transition_available[i] + ) + ) # Draw arrows from stack to tracks # arrow from stack to first track stack_width = stack_duration * svg_writer.scale_x @@ -730,21 +838,42 @@ def _draw_stack(stack, svg_writer, extra_data=()): # arrows from stack to rest of the tracks for i in range(1, len(svg_writer.trackwise_clip_count)): arrow_x_increment_per_track = 10.0 - end_arrow_offset += (svg_writer.trackwise_clip_count[i - 1] * 2.0 + 4.0) + end_arrow_offset += ( + svg_writer.trackwise_clip_count[i - 1] * 2.0 + 4.0 + ) arrow_start = Point( - (i * arrow_x_increment_per_track) + svg_writer.x_origin + stack_width * 0.5, - stack_origin.y - svg_writer.arrow_margin) + ( + (i * arrow_x_increment_per_track) + + svg_writer.x_origin + + stack_width * 0.5 + ), + stack_origin.y - svg_writer.arrow_margin + ) arrow_end = Point( - (i * arrow_x_increment_per_track) + svg_writer.x_origin + stack_width * 0.5, + ( + (i * arrow_x_increment_per_track) + + svg_writer.x_origin + + stack_width * 0.5 + ), stack_origin.y - (end_arrow_offset * svg_writer.clip_rect_height) + - svg_writer.arrow_margin) - svg_writer.draw_arrow(start_point=arrow_start, end_point=arrow_end, - stroke_width=2.0, - stroke_color=COLORS['black']) - arrow_label_text = r'children[{}]'.format(len(svg_writer.trackwise_clip_count)) - arrow_label_location = Point(arrow_start.x + svg_writer.arrow_label_margin, - stack_origin.y - svg_writer.clip_rect_height * 0.5) - svg_writer.draw_text(arrow_label_text, arrow_label_location, svg_writer.font_size) + svg_writer.arrow_margin + ) + svg_writer.draw_arrow( + start_point=arrow_start, + end_point=arrow_end, + stroke_width=2.0, + stroke_color=COLORS['black'] + ) + arrow_label_text = fr'children[{len(svg_writer.trackwise_clip_count)}]' + arrow_label_location = Point( + arrow_start.x + svg_writer.arrow_label_margin, + stack_origin.y - svg_writer.clip_rect_height * 0.5 + ) + svg_writer.draw_text( + arrow_label_text, + arrow_label_location, + svg_writer.font_size + ) # Draw range info if stack.trimmed_range() is None: trimmed_range_text = r'trimmed_range() -> {}'.format('None') @@ -758,15 +887,20 @@ def _draw_stack(stack, svg_writer, extra_data=()): source_range_text = r'source_range: {}, {}'.format( repr(float(round(stack.source_range.start_time.value, 1))), repr(float(round(stack.source_range.duration.value, 1)))) - trimmed_range_location = Point(stack_origin.x + svg_writer.font_size, - stack_origin.y + svg_writer.clip_rect_height + - svg_writer.text_margin) + trimmed_range_location = Point( + stack_origin.x + svg_writer.font_size, + stack_origin.y + svg_writer.clip_rect_height + svg_writer.text_margin + ) source_range_location = Point(stack_origin.x + svg_writer.font_size, stack_origin.y - svg_writer.font_size) svg_writer.draw_text(trimmed_range_text, trimmed_range_location, svg_writer.font_size, ) - svg_writer.draw_text(source_range_text, source_range_location, svg_writer.font_size) + svg_writer.draw_text( + source_range_text, + source_range_location, + svg_writer.font_size + ) def _draw_track(track, svg_writer, extra_data=()): @@ -788,10 +922,17 @@ def _draw_track(track, svg_writer, extra_data=()): label_size=track_text_size) time_marker_height = 0.15 * svg_writer.clip_rect_height for i in range(1, int(track_duration)): - start_pt = Point(svg_writer.x_origin + (i * svg_writer.scale_x), track_origin.y) + start_pt = Point( + svg_writer.x_origin + (i * svg_writer.scale_x), + track_origin.y + ) end_pt = Point(start_pt.x, start_pt.y + time_marker_height) - svg_writer.draw_line(start_point=start_pt, end_point=end_pt, stroke_width=1.0, - stroke_color=COLORS['black']) + svg_writer.draw_line( + start_point=start_pt, + end_point=end_pt, + stroke_width=1.0, + stroke_color=COLORS['black'] + ) item_count = 0 clip_count = 0 transition_count = 0 @@ -822,10 +963,16 @@ def _draw_track(track, svg_writer, extra_data=()): svg_writer.draw_arrow(start_point=arrow_start, end_point=arrow_end, stroke_width=2.0, stroke_color=COLORS['black']) - arrow_label_text = r'children[{}]'.format(item_count + transition_count) - arrow_label_location = Point(arrow_start.x + svg_writer.arrow_label_margin, - track_origin.y - svg_writer.clip_rect_height * 0.5) - svg_writer.draw_text(arrow_label_text, arrow_label_location, svg_writer.font_size) + arrow_label_text = fr'children[{item_count + transition_count}]' + arrow_label_location = Point( + arrow_start.x + svg_writer.arrow_label_margin, + track_origin.y - svg_writer.clip_rect_height * 0.5 + ) + svg_writer.draw_text( + arrow_label_text, + arrow_label_location, + svg_writer.font_size + ) # Draw range info if track.trimmed_range() is None: trimmed_range_text = r'trimmed_range() -> {}'.format('None') @@ -839,18 +986,22 @@ def _draw_track(track, svg_writer, extra_data=()): source_range_text = r'source_range: {}, {}'.format( repr(float(round(track.source_range.start_time.value, 1))), repr(float(round(track.source_range.duration.value, 1)))) - trimmed_range_location = Point(track_origin.x + svg_writer.font_size, - track_origin.y + svg_writer.clip_rect_height + - svg_writer.text_margin) + trimmed_range_location = Point( + track_origin.x + svg_writer.font_size, + track_origin.y + svg_writer.clip_rect_height + svg_writer.text_margin + ) source_range_location = Point(track_origin.x + svg_writer.font_size, track_origin.y - svg_writer.font_size) svg_writer.draw_text(trimmed_range_text, trimmed_range_location, svg_writer.font_size, ) - svg_writer.draw_text(source_range_text, source_range_location, svg_writer.font_size) + svg_writer.draw_text( + source_range_text, + source_range_location, + svg_writer.font_size + ) -# Draw clip def _draw_clip(clip, svg_writer, extra_data=()): clip_data = extra_data[0] clip_count = extra_data[1] @@ -862,7 +1013,7 @@ def _draw_clip(clip, svg_writer, extra_data=()): clip_rect = Rect(clip_origin, clip_data.trim_duration * svg_writer.scale_x, svg_writer.clip_rect_height) clip_text_size = 0.4 * svg_writer.clip_rect_height - clip_text = r'Clip-{}'.format(clip_data.clip_id) if len( + clip_text = fr'Clip-{clip_data.clip_id}' if len( clip.name) == 0 else clip.name svg_writer.draw_labeled_solid_rect_with_border( clip_rect, @@ -870,10 +1021,16 @@ def _draw_clip(clip, svg_writer, extra_data=()): label_size=clip_text_size) time_marker_height = 0.15 * svg_writer.clip_rect_height for i in range(int(clip_data.src_start), int(clip_data.src_end) + 1): - start_pt = Point(svg_writer.x_origin + (i * svg_writer.scale_x), clip_origin.y) + start_pt = Point( + svg_writer.x_origin + (i * svg_writer.scale_x), clip_origin.y + ) end_pt = Point(start_pt.x, start_pt.y + time_marker_height) - svg_writer.draw_line(start_point=start_pt, end_point=end_pt, stroke_width=1.0, - stroke_color=COLORS['black']) + svg_writer.draw_line( + start_point=start_pt, + end_point=end_pt, + stroke_width=1.0, + stroke_color=COLORS['black'] + ) # Draw range info if clip.trimmed_range() is None: trimmed_range_text = r'trimmed_range() -> {}'.format('None') @@ -912,7 +1069,7 @@ def _draw_clip(clip, svg_writer, extra_data=()): Rect(media_origin, clip_data.avlbl_duration * svg_writer.scale_x, svg_writer.clip_rect_height)) media_text_size = 0.4 * svg_writer.clip_rect_height - media_text = r'Media-{}'.format(clip_data.clip_id) if len( + media_text = fr'Media-{clip_data.clip_id}' if len( clip.media_reference.name) == 0 else clip.media_reference.name svg_writer.draw_labeled_solid_rect_with_border( Rect(trim_media_origin, clip_data.trim_duration * svg_writer.scale_x, @@ -943,7 +1100,7 @@ def _draw_clip(clip, svg_writer, extra_data=()): if clip.media_reference.target_url is None: target_url_text = r'target_url: {}'.format('Media Unavailable') else: - target_url_text = r'target_url: {}'.format(clip.media_reference.target_url) + target_url_text = fr'target_url: {clip.media_reference.target_url}' target_url_location = Point(media_origin.x + svg_writer.font_size, media_origin.y - 2.0 * svg_writer.font_size) svg_writer.draw_text(target_url_text, target_url_location, svg_writer.font_size) @@ -1122,7 +1279,7 @@ def convert_otio_to_svg(timeline, width, height): font_family='sans-serif', image_margin=20.0, font_size=15.0, arrow_label_margin=5.0) random_colors_used = [] - seed(100) + random.seed(100) draw_item(timeline, svg_writer, ()) return svg_writer.get_image() diff --git a/src/py-opentimelineio/opentimelineio/algorithms/__init__.py b/src/py-opentimelineio/opentimelineio/algorithms/__init__.py index ea662763a..d6f2eaea2 100644 --- a/src/py-opentimelineio/opentimelineio/algorithms/__init__.py +++ b/src/py-opentimelineio/opentimelineio/algorithms/__init__.py @@ -1,26 +1,5 @@ -# +# SPDX-License-Identifier: Apache-2.0 # Copyright Contributors to the OpenTimelineIO project -# -# Licensed under the Apache License, Version 2.0 (the "Apache License") -# with the following modification; you may not use this file except in -# compliance with the Apache License and the following modification to it: -# Section 6. Trademarks. is deleted and replaced with: -# -# 6. Trademarks. This License does not grant permission to use the trade -# names, trademarks, service marks, or product names of the Licensor -# and its affiliates, except as required to comply with Section 4(c) of -# the License and to reproduce the content of the NOTICE file. -# -# You may obtain a copy of the Apache License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the Apache License with the above modification is -# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -# KIND, either express or implied. See the Apache License for the specific -# language governing permissions and limitations under the Apache License. -# """Algorithms for OTIO objects.""" diff --git a/src/py-opentimelineio/opentimelineio/algorithms/filter.py b/src/py-opentimelineio/opentimelineio/algorithms/filter.py index 5df39f78a..57243de2c 100644 --- a/src/py-opentimelineio/opentimelineio/algorithms/filter.py +++ b/src/py-opentimelineio/opentimelineio/algorithms/filter.py @@ -1,27 +1,5 @@ -#!/usr/bin/env python -# +# SPDX-License-Identifier: Apache-2.0 # Copyright Contributors to the OpenTimelineIO project -# -# Licensed under the Apache License, Version 2.0 (the "Apache License") -# with the following modification; you may not use this file except in -# compliance with the Apache License and the following modification to it: -# Section 6. Trademarks. is deleted and replaced with: -# -# 6. Trademarks. This License does not grant permission to use the trade -# names, trademarks, service marks, or product names of the Licensor -# and its affiliates, except as required to comply with Section 4(c) of -# the License and to reproduce the content of the NOTICE file. -# -# You may obtain a copy of the Apache License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the Apache License with the above modification is -# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -# KIND, either express or implied. See the Apache License for the specific -# language governing permissions and limitations under the Apache License. -# """Algorithms for filtering OTIO files. """ @@ -45,23 +23,31 @@ def filtered_composition( unary_filter_fn, types_to_prune=None, ): - """Filter a deep copy of root (and children) with unary_filter_fn. + """ + Filter a deep copy of root (and children) with ``unary_filter_fn``. + + The ``unary_filter_fn`` must have this signature: + + .. py:function:: func(item: typing.Any) -> list[typing.Any] + :noindex: - types_to_prune:: tuple of types, example: (otio.schema.Gap,...) 1. Make a deep copy of root 2. Starting with root, perform a depth first traversal 3. For each item (including root): - a. if types_to_prune is not None and item is an instance of a type - in types_to_prune, prune it from the copy, continue. - b. Otherwise, pass the copy to unary_filter_fn. If unary_filter_fn: - I. returns an object: add it to the copy, replacing original - II. returns a tuple: insert it into the list, replacing original - III. returns None: prune it + + a. If ``types_to_prune`` is not None and item is an instance of a type in + ``types_to_prune``, prune it from the copy, continue. + b. Otherwise, pass the copy to ``unary_filter_fn``. If ``unary_filter_fn``: + + I. Returns an object: add it to the copy, replacing original + II. Returns a tuple: insert it into the list, replacing original + III. Returns None: prune it 4. If an item is pruned, do not traverse its children 5. Return the new deep copy. - EXAMPLE 1 (filter): + Example 1 (filter):: + If your unary function is: def fn(thing): if thing.name == B: @@ -73,7 +59,8 @@ def fn(thing): filtered_composition(track, fn) => [A,B',C] - EXAMPLE 2 (prune): + Example 2 (prune):: + If your unary function is: def fn(thing): if thing.name == B: @@ -83,7 +70,8 @@ def fn(thing): filtered_composition(track, fn) => [A,C] - EXAMPLE 3 (expand): + Example 3 (expand):: + If your unary function is: def fn(thing): if thing.name == B: @@ -93,10 +81,15 @@ def fn(thing): filtered_composition(track, fn) => [A,B_1,B_2,B_3,C] - EXAMPLE 4 (prune gaps): + Example 4 (prune gaps):: + track :: [Gap, A, Gap] filtered_composition( track, lambda _:_, types_to_prune=(otio.schema.Gap,)) => [A] + + :param SerializableObjectWithMetadata root: Object to filter on + :param unary_filter_fn: Filter function + :param tuple(type) types_to_prune: Types to prune. Example: (otio.schema.Gap,...) """ # deep copy everything @@ -109,7 +102,7 @@ def fn(thing): if isinstance(mutable_object, schema.Timeline): header_list.append(mutable_object.tracks) - iter_list = header_list + list(mutable_object.each_child()) + iter_list = header_list + list(mutable_object.find_children()) for child in iter_list: if _safe_parent(child) is not None and _is_in(child.parent(), prune_list): @@ -157,27 +150,35 @@ def filtered_with_sequence_context( reduce_fn, types_to_prune=None, ): - """Filter a deep copy of root (and children) with reduce_fn. + """Filter a deep copy of root (and children) with ``reduce_fn``. - reduce_fn::function(previous_item, current, next_item) (see below) - types_to_prune:: tuple of types, example: (otio.schema.Gap,...) + The ``reduce_fn`` must have this signature: + + .. currentmodule:: _ + + .. py:function:: func(previous_item: typing.Any, current: typing.Any, next_item: typing.Any) -> list[typing.Any] # noqa + :noindex: 1. Make a deep copy of root 2. Starting with root, perform a depth first traversal 3. For each item (including root): - a. if types_to_prune is not None and item is an instance of a type - in types_to_prune, prune it from the copy, continue. - b. Otherwise, pass (prev, copy, and next) to reduce_fn. If reduce_fn: - I. returns an object: add it to the copy, replacing original - II. returns a tuple: insert it into the list, replacing original - III. returns None: prune it - - ** note that reduce_fn is always passed objects from the original - deep copy, not what prior calls return. See below for examples + + a. if types_to_prune is not None and item is an instance of a type + in types_to_prune, prune it from the copy, continue. + b. Otherwise, pass (prev, copy, and next) to reduce_fn. If ``reduce_fn``: + + I. returns an object: add it to the copy, replacing original + II. returns a tuple: insert it into the list, replacing original + III. returns None: prune it + + .. note:: ``reduce_fn`` is always passed objects from the original + deep copy, not what prior calls return. See below for examples + 4. If an item is pruned, do not traverse its children 5. Return the new deep copy. - EXAMPLE 1 (filter): + Example 1 (filter):: + >>> track = [A,B,C] >>> def fn(prev_item, thing, next_item): ... if prev_item.name == A: @@ -191,7 +192,8 @@ def filtered_with_sequence_context( fn(A, B, C) => D fn(B, C, D) => C # !! note that it was passed B instead of D. - EXAMPLE 2 (prune): + Example 2 (prune):: + >>> track = [A,B,C] >>> def fn(prev_item, thing, next_item): ... if prev_item.name == A: @@ -205,7 +207,8 @@ def filtered_with_sequence_context( fn(A, B, C) => None fn(B, C, D) => C # !! note that it was passed B instead of D. - EXAMPLE 3 (expand): + Example 3 (expand):: + >>> def fn(prev_item, thing, next_item): ... if prev_item.name == A: ... return (D, E) # tuple of new clips @@ -217,6 +220,10 @@ def filtered_with_sequence_context( fn(None, A, B) => A fn(A, B, C) => (D, E) fn(B, C, D) => C # !! note that it was passed B instead of D. + + :param SerializableObjectWithMetadata root: Object to filter on + :param reduce_fn: Filter function + :param tuple(type) types_to_prune: Types to prune. Example: (otio.schema.Gap,...) """ # deep copy everything @@ -229,7 +236,7 @@ def filtered_with_sequence_context( if isinstance(mutable_object, schema.Timeline): header_list.append(mutable_object.tracks) - iter_list = header_list + list(mutable_object.each_child()) + iter_list = header_list + list(mutable_object.find_children()) # expand to include prev, next when appropriate expanded_iter_list = [] diff --git a/src/py-opentimelineio/opentimelineio/algorithms/stack_algo.py b/src/py-opentimelineio/opentimelineio/algorithms/stack_algo.py index c0b36ecfe..687517595 100644 --- a/src/py-opentimelineio/opentimelineio/algorithms/stack_algo.py +++ b/src/py-opentimelineio/opentimelineio/algorithms/stack_algo.py @@ -1,26 +1,5 @@ -# +# SPDX-License-Identifier: Apache-2.0 # Copyright Contributors to the OpenTimelineIO project -# -# Licensed under the Apache License, Version 2.0 (the "Apache License") -# with the following modification; you may not use this file except in -# compliance with the Apache License and the following modification to it: -# Section 6. Trademarks. is deleted and replaced with: -# -# 6. Trademarks. This License does not grant permission to use the trade -# names, trademarks, service marks, or product names of the Licensor -# and its affiliates, except as required to comply with Section 4(c) of -# the License and to reproduce the content of the NOTICE file. -# -# You may obtain a copy of the Apache License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the Apache License with the above modification is -# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -# KIND, either express or implied. See the Apache License for the specific -# language governing permissions and limitations under the Apache License. -# __doc__ = """ Algorithms for stack objects. """ @@ -32,15 +11,21 @@ def top_clip_at_time(in_stack, t): - """Return the topmost visible child that overlaps with time t. + """Return the topmost visible child that overlaps with time ``t``. - Example: - tr1: G1, A, G2 - tr2: [B------] - G1, and G2 are gaps, A and B are clips. + Example:: - If t is within A, a will be returned. If t is within G1 or G2, B will be - returned. + tr1: G1, A, G2 + tr2: [B------] + G1, and G2 are gaps, A and B are clips. + + If ``t`` is within ``A``, ``A`` will be returned. If ``t`` is within ``G1`` or + ``G2``, ``B`` will be returned. + + :param Stack in_stack: Stack + :param RationalTime t: Time + :returns: Top clip + :rtype: Clip """ # ensure that it only runs on stacks @@ -52,7 +37,7 @@ def top_clip_at_time(in_stack, t): ) ) - # build a range to use the `each_child`method. + # build a range to use the `find_clips`method. search_range = opentime.TimeRange( start_time=t, # 0 duration so we are just sampling a point in time. @@ -64,9 +49,9 @@ def top_clip_at_time(in_stack, t): # walk through the children of the stack in reverse order. for track in reversed(in_stack): valid_results = [] - if hasattr(track, "each_child"): + if hasattr(track, "find_clips"): valid_results = list( - c for c in track.each_clip(search_range, shallow_search=True) + c for c in track.find_clips(search_range, shallow_search=True) if c.visible() ) diff --git a/src/py-opentimelineio/opentimelineio/algorithms/timeline_algo.py b/src/py-opentimelineio/opentimelineio/algorithms/timeline_algo.py index 790d8edc8..3c97d5594 100644 --- a/src/py-opentimelineio/opentimelineio/algorithms/timeline_algo.py +++ b/src/py-opentimelineio/opentimelineio/algorithms/timeline_algo.py @@ -1,26 +1,5 @@ -# +# SPDX-License-Identifier: Apache-2.0 # Copyright Contributors to the OpenTimelineIO project -# -# Licensed under the Apache License, Version 2.0 (the "Apache License") -# with the following modification; you may not use this file except in -# compliance with the Apache License and the following modification to it: -# Section 6. Trademarks. is deleted and replaced with: -# -# 6. Trademarks. This License does not grant permission to use the trade -# names, trademarks, service marks, or product names of the Licensor -# and its affiliates, except as required to comply with Section 4(c) of -# the License and to reproduce the content of the NOTICE file. -# -# You may obtain a copy of the Apache License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the Apache License with the above modification is -# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -# KIND, either express or implied. See the Apache License for the specific -# language governing permissions and limitations under the Apache License. -# """Algorithms for timeline objects.""" @@ -32,12 +11,22 @@ def timeline_trimmed_to_range(in_timeline, trim_range): - """Returns a new timeline that is a copy of the in_timeline, but with items + """ + Returns a new timeline that is a copy of the in_timeline, but with items outside the trim_range removed and items on the ends trimmed to the - trim_range. Note that the timeline is never expanded, only shortened. + trim_range. + + .. note:: the timeline is never expanded, only shortened. + Please note that you could do nearly the same thing non-destructively by - just setting the Track's source_range but sometimes you want to really cut - away the stuff outside and that's what this function is meant for.""" + just setting the :py:class:`.Track`\'s source_range but sometimes you want to + really cut away the stuff outside and that's what this function is meant for. + + :param Timeline in_timeline: Timeline to trim + :param TimeRange trim_range: + :returnd: New trimmed timeline + :rtype: Timeline + """ new_timeline = copy.deepcopy(in_timeline) for track_num, child_track in enumerate(in_timeline.tracks): diff --git a/src/py-opentimelineio/opentimelineio/algorithms/track_algo.py b/src/py-opentimelineio/opentimelineio/algorithms/track_algo.py index b411a7150..745fc6ef2 100644 --- a/src/py-opentimelineio/opentimelineio/algorithms/track_algo.py +++ b/src/py-opentimelineio/opentimelineio/algorithms/track_algo.py @@ -1,26 +1,5 @@ -# +# SPDX-License-Identifier: Apache-2.0 # Copyright Contributors to the OpenTimelineIO project -# -# Licensed under the Apache License, Version 2.0 (the "Apache License") -# with the following modification; you may not use this file except in -# compliance with the Apache License and the following modification to it: -# Section 6. Trademarks. is deleted and replaced with: -# -# 6. Trademarks. This License does not grant permission to use the trade -# names, trademarks, service marks, or product names of the Licensor -# and its affiliates, except as required to comply with Section 4(c) of -# the License and to reproduce the content of the NOTICE file. -# -# You may obtain a copy of the Apache License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the Apache License with the above modification is -# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -# KIND, either express or implied. See the Apache License for the specific -# language governing permissions and limitations under the Apache License. -# """Algorithms for track objects.""" @@ -34,12 +13,22 @@ def track_trimmed_to_range(in_track, trim_range): - """Returns a new track that is a copy of the in_track, but with items + """ + Returns a new track that is a copy of the in_track, but with items outside the trim_range removed and items on the ends trimmed to the - trim_range. Note that the track is never expanded, only shortened. + trim_range. + + .. note:: The track is never expanded, only shortened. + Please note that you could do nearly the same thing non-destructively by - just setting the Track's source_range but sometimes you want to really cut - away the stuff outside and that's what this function is meant for.""" + just setting the :py:class:`.Track`\'s source_range but sometimes you want + to really cut away the stuff outside and that's what this function is meant for. + + :param Track in_track: Track to trim + :param TimeRange trim_range: + :returns: New trimmed track + :rtype: Track + """ new_track = copy.deepcopy(in_track) track_map = new_track.range_of_all_children() @@ -92,15 +81,22 @@ def track_with_expanded_transitions(in_track): """Expands transitions such that neighboring clips are trimmed into regions of overlap. - For example, if your track is: + For example, if your track is:: + Clip1, T, Clip2 - will return: + will return:: + Clip1', (Clip1_t, T, Clip2_t), Clip2' - Where Clip1' is the part of Clip1 not in the transition, Clip1_t is the - part inside the transition and so on. Please note that the items used in - a transition are encapsulated in `tuple`s + Where ``Clip1'`` is the part of ``Clip1`` not in the transition, ``Clip1_t`` is the + part inside the transition and so on. + + .. note:: The items used in a transition are encapsulated in tuples. + + :param Track in_track: Track to expand + :returns: Track + :rtype: list[Track] """ result_track = [] @@ -166,12 +162,12 @@ def _expand_transition(target_transition, from_track): ) if target_transition.in_offset is None: raise RuntimeError( - "in_offset is None on: {}".format(target_transition) + f"in_offset is None on: {target_transition}" ) if target_transition.out_offset is None: raise RuntimeError( - "out_offset is None on: {}".format(target_transition) + f"out_offset is None on: {target_transition}" ) pre.name = (pre.name or "") + "_transition_pre" diff --git a/src/py-opentimelineio/opentimelineio/console/__init__.py b/src/py-opentimelineio/opentimelineio/console/__init__.py index e5032190e..46208305b 100644 --- a/src/py-opentimelineio/opentimelineio/console/__init__.py +++ b/src/py-opentimelineio/opentimelineio/console/__init__.py @@ -1,26 +1,5 @@ -# +# SPDX-License-Identifier: Apache-2.0 # Copyright Contributors to the OpenTimelineIO project -# -# Licensed under the Apache License, Version 2.0 (the "Apache License") -# with the following modification; you may not use this file except in -# compliance with the Apache License and the following modification to it: -# Section 6. Trademarks. is deleted and replaced with: -# -# 6. Trademarks. This License does not grant permission to use the trade -# names, trademarks, service marks, or product names of the Licensor -# and its affiliates, except as required to comply with Section 4(c) of -# the License and to reproduce the content of the NOTICE file. -# -# You may obtain a copy of the Apache License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the Apache License with the above modification is -# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -# KIND, either express or implied. See the Apache License for the specific -# language governing permissions and limitations under the Apache License. -# """Console scripts for OpenTimelineIO @@ -34,6 +13,7 @@ otioconvert, otiocat, otiostat, + otiotool, console_utils, autogen_serialized_datamodel, otiopluginfo, diff --git a/src/py-opentimelineio/opentimelineio/console/autogen_plugin_documentation.py b/src/py-opentimelineio/opentimelineio/console/autogen_plugin_documentation.py index d8e5ef87c..dabb3a349 100644 --- a/src/py-opentimelineio/opentimelineio/console/autogen_plugin_documentation.py +++ b/src/py-opentimelineio/opentimelineio/console/autogen_plugin_documentation.py @@ -1,27 +1,7 @@ #!/usr/bin/env python # +# SPDX-License-Identifier: Apache-2.0 # Copyright Contributors to the OpenTimelineIO project -# -# licensed under the apache license, version 2.0 (the "apache license") -# with the following modification; you may not use this file except in -# compliance with the apache license and the following modification to it: -# section 6. trademarks. is deleted and replaced with: -# -# 6. trademarks. this license does not grant permission to use the trade -# names, trademarks, service marks, or product names of the licensor -# and its affiliates, except as required to comply with section 4(c) of -# the license and to reproduce the content of the notice file. -# -# you may obtain a copy of the apache license at -# -# http://www.apache.org/licenses/license-2.0 -# -# unless required by applicable law or agreed to in writing, software -# distributed under the apache license with the above modification is -# distributed on an "as is" basis, without warranties or conditions of any -# kind, either express or implied. see the apache license for the specific -# language governing permissions and limitations under the apache license. -# """Generates documentation of all the built in plugins for OpenTimelineIO""" @@ -32,12 +12,7 @@ import tempfile import textwrap -try: - # python2 - import StringIO as io -except ImportError: - # python3 - import io +import io import opentimelineio as otio @@ -92,11 +67,9 @@ Adapter plugins convert to and from OpenTimelineIO. - Adapters documentation page for more -information +[Adapters documentation page for more information](./adapters). -Tutorial on how to write an -adapter. +[Tutorial on how to write an adapter](write-an-adapter). {adapters} @@ -105,8 +78,7 @@ Media Linkers run after the adapter has read in the file and convert the media references into valid references where appropriate. - Tutorial on how to write a -Media Linker +[Tutorial on how to write a Media Linker](write-a-media-linker). {media_linkers} @@ -114,8 +86,7 @@ SchemaDef plugins define new external schema. - Tutorial on how to write a -schemadef +[Tutorial on how to write a schemadef](write-a-schemadef). {schemadefs} @@ -123,8 +94,7 @@ HookScripts are extra plugins that run on _hooks_. -Tutorial on how to write a -hookscript. +[Tutorial on how to write a hookscript](write-a-hookscript). {hook_scripts} @@ -264,15 +234,15 @@ def _format_adapters(plugin_map): doc = feature_data['doc'] if doc: feature_lines.append( - _format_doc(doc, "- {}: \n```\n".format(feature)) + "\n```" + _format_doc(doc, f"- {feature}: \n```\n") + "\n```" ) else: feature_lines.append( - "- {}:".format(feature) + f"- {feature}:" ) for arg in feature_data["args"]: - feature_lines.append(" - {}".format(arg)) + feature_lines.append(f" - {arg}") return ADAPTER_TEMPLATE.format("\n".join(feature_lines)) @@ -284,10 +254,10 @@ def _format_schemadefs(plugin_map): doc = plugin_map['SchemaDefs'][sd]['doc'] if doc: feature_lines.append( - _format_doc(doc, "- {}: \n```\n".format(sd)) + "\n```" + _format_doc(doc, f"- {sd}: \n```\n") + "\n```" ) else: - feature_lines.append("- {}:".format(sd)) + feature_lines.append(f"- {sd}:") return SCHEMADEF_TEMPLATE.format("\n".join(feature_lines)) @@ -331,7 +301,7 @@ def _manifest_formatted( pt_lines.append(plug_lines) - display_map[pt] = "\n".join((str(line) for line in pt_lines)) + display_map[pt] = "\n".join(str(line) for line in pt_lines) return MANIFEST_CONTENT_TEMPLATE.format( adapters=display_map['adapters'], @@ -365,7 +335,7 @@ def generate_and_write_documentation_plugins( for p in manifest_path_list ] - manifest_list = "\n".join("- `{}`".format(mp) for mp in sanitized_paths) + manifest_list = "\n".join(f"- `{mp}`" for mp in sanitized_paths) core_manifest_path = manifest_path_list[0] core_manifest_path_sanitized = sanitized_paths[0] @@ -388,7 +358,7 @@ def generate_and_write_documentation_plugins( local_manifest_paths = manifest_path_list[2:] local_manifest_paths_sanitized = sanitized_paths[2:] local_manifest_list = "\n".join( - "- `{}`".format(mp) for mp in local_manifest_paths_sanitized + f"- `{mp}`" for mp in local_manifest_paths_sanitized ) local_manifest_body = _manifest_formatted( plugin_info_map, @@ -443,7 +413,7 @@ def main(): with open(output, 'w') as fo: fo.write(docs) - print("wrote documentation to {}.".format(output)) + print(f"wrote documentation to {output}.") if __name__ == '__main__': diff --git a/src/py-opentimelineio/opentimelineio/console/autogen_serialized_datamodel.py b/src/py-opentimelineio/opentimelineio/console/autogen_serialized_datamodel.py index b8508136a..0153afdd6 100644 --- a/src/py-opentimelineio/opentimelineio/console/autogen_serialized_datamodel.py +++ b/src/py-opentimelineio/opentimelineio/console/autogen_serialized_datamodel.py @@ -1,27 +1,7 @@ #!/usr/bin/env python # +# SPDX-License-Identifier: Apache-2.0 # Copyright Contributors to the OpenTimelineIO project -# -# Licensed under the Apache License, Version 2.0 (the "Apache License") -# with the following modification; you may not use this file except in -# compliance with the Apache License and the following modification to it: -# Section 6. Trademarks. is deleted and replaced with: -# -# 6. Trademarks. This License does not grant permission to use the trade -# names, trademarks, service marks, or product names of the Licensor -# and its affiliates, except as required to comply with Section 4(c) of -# the License and to reproduce the content of the NOTICE file. -# -# You may obtain a copy of the Apache License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the Apache License with the above modification is -# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -# KIND, either express or implied. See the Apache License for the specific -# language governing permissions and limitations under the Apache License. -# """Generates documentation of the serialized data model for OpenTimelineIO.""" @@ -31,13 +11,9 @@ import json import tempfile import sys +import textwrap -try: - # python2 - import StringIO as io -except ImportError: - # python3 - import io +import io import opentimelineio as otio @@ -45,7 +21,8 @@ DOCUMENT_HEADER = """# Serialized Data Documentation This documents all the OpenTimelineIO classes that serialize to and from JSON, -omitting SchemaDef plugins. This document is automatically generated by running: +omitting SchemaDef plugins. This document is automatically generated by +running: `src/py-opentimelineio/opentimelineio/console/autogen_serialized_datamodel.py` @@ -172,7 +149,7 @@ def _generate_model_for_module(mod, classes, modules): modules.add(mod) # fetch the classes from this module - serializeable_classes = [ + serializable_classes = [ thing for thing in mod.__dict__.values() if ( inspect.isclass(thing) @@ -188,7 +165,7 @@ def _generate_model_for_module(mod, classes, modules): # serialize/deserialize the classes to capture their serialized parameters model = {} - for cl in serializeable_classes: + for cl in serializable_classes: if cl in SKIP_CLASSES: continue @@ -208,7 +185,7 @@ def _generate_model_for_module(mod, classes, modules): except AttributeError: pass else: - sys.stderr.write("ERROR: could not fetch property: {}".format(k)) + sys.stderr.write(f"ERROR: could not fetch property: {k}") # Stashing the OTIO_SCHEMA back into the dictionary since the # documentation uses this information in its header. @@ -274,12 +251,10 @@ def _remap_to_python_modules(cl): ] # the C++ modules - IGNORE_MODS = set( - [ - otio._otio, - otio._opentime - ] - ) + IGNORE_MODS = { + otio._otio, + otio._opentime + } for mod in SEARCH_MODULES: result = _search_mod_recursively(cl, mod, IGNORE_MODS) @@ -318,11 +293,29 @@ def _write_documentation(model): for cl in sorted(modules[module_list], key=lambda cl: str(cl)): modname = this_mod label = model[cl]["OTIO_SCHEMA"] + + if (cl.__doc__ is not None): + docstring = cl.__doc__.split("\n") + new_docstring = [] + for line in docstring: + line = textwrap.wrap(line, width=100, + expand_tabs=False, + replace_whitespace=False, + drop_whitespace=False, + break_long_words=False) + if (line == []): + line = [""] + for wrapped_line in line: + new_docstring.append(wrapped_line) + new_docstring = "\n".join(new_docstring) + else: + new_docstring = cl.__doc__ + md_with_helpstrings.write( CLASS_HEADER_WITH_DOCS.format( classname=label, modpath=modname + "." + cl.__name__, - docstring=cl.__doc__ + docstring=new_docstring ) ) md_only_fields.write( @@ -372,7 +365,7 @@ def main(): with open(output_only_fields, 'w') as fo: fo.write(without_docs) - print("wrote documentation to {} and {}".format(output, output_only_fields)) + print(f"wrote documentation to {output} and {output_only_fields}") def generate_and_write_documentation(): diff --git a/src/py-opentimelineio/opentimelineio/console/autogen_version_map.py b/src/py-opentimelineio/opentimelineio/console/autogen_version_map.py new file mode 100644 index 000000000..6d6a94772 --- /dev/null +++ b/src/py-opentimelineio/opentimelineio/console/autogen_version_map.py @@ -0,0 +1,116 @@ +#!/usr/bin/env python +# +# SPDX-License-Identifier: Apache-2.0 +# Copyright Contributors to the OpenTimelineIO project + +"""Generate the CORE_VERSION_MAP for this version of OTIO""" + +import argparse +import tempfile + +import opentimelineio as otio + + +LABEL_MAP_TEMPLATE = """{{ "{label}", + {{ +{sv_map} + }} }}, + // {{next}}""" +MAP_ITEM_TEMPLATE = '{indent}{{ "{key}", {value} }},' +INDENT = 10 + + +def _parsed_args(): + parser = argparse.ArgumentParser( + description=__doc__, + formatter_class=argparse.ArgumentDefaultsHelpFormatter + ) + parser.add_argument( + "-d", + "--dryrun", + default=False, + action="store_true", + help="write to stdout instead of printing to file." + ) + parser.add_argument( + "-l", + "--label", + default=otio.__version__, + # @TODO - should we strip the .dev1 label? that would probably be + # more consistent since we don't do sub-beta releases + help="Version label to assign this schema map to." + ) + parser.add_argument( + "-i", + "--input", + type=str, + default=None, + required=True, + help="Path to CORE_VERSION_MAP.last.cpp" + ) + parser.add_argument( + "-o", + "--output", + type=str, + default=None, + help="Path to where CORE_VERSION_MAP.cpp should be written to." + ) + + return parser.parse_args() + + +def generate_core_version_map(src_text, label, version_map): + # turn the braces in the .cpp file into python-format template compatible + # form ({{ }} where needed) + src_text = src_text.replace("{", "{{").replace("}", "}}") + src_text = src_text.replace("// {{next}}", "{next}") + + # iterate over the map and print the template out + map_text = [] + for key, value in sorted(version_map.items()): + map_text.append( + MAP_ITEM_TEMPLATE.format( + indent=' ' * INDENT, + key=key, + value=value + ) + ) + map_text = '\n'.join(map_text) + + # assemble the result + next_text = LABEL_MAP_TEMPLATE.format(label=label, sv_map=map_text) + return src_text.format(label=label, next=next_text) + + +def main(): + args = _parsed_args() + + with open(args.input) as fi: + input = fi.read() + + result = generate_core_version_map( + input, + args.label, + otio.core.type_version_map() + ) + + if args.dryrun: + print(result) + return + + output = args.output + if not output: + output = tempfile.NamedTemporaryFile( + 'w', + suffix="CORE_VERSION_MAP.cpp", + delete=False + ).name + + with open(output, 'w', newline="\n") as fo: + fo.write(result) + + print(f"Wrote CORE_VERSION_MAP to: '{output}'.") + + +if __name__ == '__main__': + main() diff --git a/src/py-opentimelineio/opentimelineio/console/console_utils.py b/src/py-opentimelineio/opentimelineio/console/console_utils.py index b7c27ed28..c6b923f22 100644 --- a/src/py-opentimelineio/opentimelineio/console/console_utils.py +++ b/src/py-opentimelineio/opentimelineio/console/console_utils.py @@ -1,26 +1,5 @@ -# +# SPDX-License-Identifier: Apache-2.0 # Copyright Contributors to the OpenTimelineIO project -# -# Licensed under the Apache License, Version 2.0 (the "Apache License") -# with the following modification; you may not use this file except in -# compliance with the Apache License and the following modification to it: -# Section 6. Trademarks. is deleted and replaced with: -# -# 6. Trademarks. This License does not grant permission to use the trade -# names, trademarks, service marks, or product names of the Licensor -# and its affiliates, except as required to comply with Section 4(c) of -# the License and to reproduce the content of the NOTICE file. -# -# You may obtain a copy of the Apache License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the Apache License with the above modification is -# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -# KIND, either express or implied. See the Apache License for the specific -# language governing permissions and limitations under the Apache License. -# import ast diff --git a/src/py-opentimelineio/opentimelineio/console/otiocat.py b/src/py-opentimelineio/opentimelineio/console/otiocat.py index ac5fa83df..027edcda8 100755 --- a/src/py-opentimelineio/opentimelineio/console/otiocat.py +++ b/src/py-opentimelineio/opentimelineio/console/otiocat.py @@ -1,27 +1,7 @@ #!/usr/bin/env python # +# SPDX-License-Identifier: Apache-2.0 # Copyright Contributors to the OpenTimelineIO project -# -# Licensed under the Apache License, Version 2.0 (the "Apache License") -# with the following modification; you may not use this file except in -# compliance with the Apache License and the following modification to it: -# Section 6. Trademarks. is deleted and replaced with: -# -# 6. Trademarks. This License does not grant permission to use the trade -# names, trademarks, service marks, or product names of the Licensor -# and its affiliates, except as required to comply with Section 4(c) of -# the License and to reproduce the content of the NOTICE file. -# -# You may obtain a copy of the Apache License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the Apache License with the above modification is -# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -# KIND, either express or implied. See the Apache License for the specific -# language governing permissions and limitations under the Apache License. -# """Print the contents of an OTIO file to stdout.""" diff --git a/src/py-opentimelineio/opentimelineio/console/otioconvert.py b/src/py-opentimelineio/opentimelineio/console/otioconvert.py index 7f37c0e5b..d5828acb3 100755 --- a/src/py-opentimelineio/opentimelineio/console/otioconvert.py +++ b/src/py-opentimelineio/opentimelineio/console/otioconvert.py @@ -1,27 +1,7 @@ #!/usr/bin/env python # +# SPDX-License-Identifier: Apache-2.0 # Copyright Contributors to the OpenTimelineIO project -# -# Licensed under the Apache License, Version 2.0 (the "Apache License") -# with the following modification; you may not use this file except in -# compliance with the Apache License and the following modification to it: -# Section 6. Trademarks. is deleted and replaced with: -# -# 6. Trademarks. This License does not grant permission to use the trade -# names, trademarks, service marks, or product names of the Licensor -# and its affiliates, except as required to comply with Section 4(c) of -# the License and to reproduce the content of the NOTICE file. -# -# You may obtain a copy of the Apache License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the Apache License with the above modification is -# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -# KIND, either express or implied. See the Apache License for the specific -# language governing permissions and limitations under the Apache License. -# import argparse import sys @@ -175,7 +155,7 @@ def _parsed_args(): # print version information to the shell if result.version: - print("OpenTimelineIO version: {}".format(otio.__version__)) + print(f"OpenTimelineIO version: {otio.__version__}") if pkg_resources: pkg_resource_plugins = list( @@ -184,7 +164,7 @@ def _parsed_args(): if pkg_resource_plugins: print("Plugins from pkg_resources:") for plugin in pkg_resource_plugins: - print(" {}".format(plugin.dist)) + print(f" {plugin.dist}") else: print("No pkg_resource plugins installed.") parser.exit() @@ -275,7 +255,7 @@ def main(): for track in args.tracks.split(","): tr = result_tl.tracks[int(track)] del result_tl.tracks[int(track)] - print("track {0} is of kind: '{1}'".format(track, tr.kind)) + print(f"track {track} is of kind: '{tr.kind}'") result_tracks.append(tr) result_tl.tracks = result_tracks diff --git a/src/py-opentimelineio/opentimelineio/console/otiopluginfo.py b/src/py-opentimelineio/opentimelineio/console/otiopluginfo.py index ab44ba1d5..ffcd1fb7f 100644 --- a/src/py-opentimelineio/opentimelineio/console/otiopluginfo.py +++ b/src/py-opentimelineio/opentimelineio/console/otiopluginfo.py @@ -1,27 +1,7 @@ #!/usr/bin/env python # +# SPDX-License-Identifier: Apache-2.0 # Copyright Contributors to the OpenTimelineIO project -# -# licensed under the apache license, version 2.0 (the "apache license") -# with the following modification; you may not use this file except in -# compliance with the apache license and the following modification to it: -# section 6. trademarks. is deleted and replaced with: -# -# 6. trademarks. this license does not grant permission to use the trade -# names, trademarks, service marks, or product names of the licensor -# and its affiliates, except as required to comply with section 4(c) of -# the license and to reproduce the content of the notice file. -# -# you may obtain a copy of the apache license at -# -# http://www.apache.org/licenses/license-2.0 -# -# unless required by applicable law or agreed to in writing, software -# distributed under the apache license with the above modification is -# distributed on an "as is" basis, without warranties or conditions of any -# kind, either express or implied. see the apache license for the specific -# language governing permissions and limitations under the apache license. -# """Print information about the OTIO plugin ecosystem.""" @@ -104,8 +84,8 @@ def _supported_features_formatted(feature_map, _): extra_features = [] for kind in ["read", "write"]: if ( - "{}_from_string".format(kind) in feature_map - and "{}_from_file".format(kind) not in feature_map + f"{kind}_from_string" in feature_map + and f"{kind}_from_file" not in feature_map ): extra_features.append( "{0}_from_file (calls: {0}_from_string)".format(kind) @@ -114,13 +94,13 @@ def _supported_features_formatted(feature_map, _): if extra_features: print(" implicit supported features:") for feat in extra_features: - print(" {}".format(feat)) + print(f" {feat}") def _schemadefs_formatted(feature_map, args): print(" SchemaDefs:") for sd in feature_map.keys(): - print(" {}".format(sd)) + print(f" {sd}") _docs_formatted(feature_map[sd]['doc'], args, indent=8) @@ -183,7 +163,7 @@ def _print_field(key, val, **args): _FORMATTER[key](val, args) return - print(" {}: {}".format(key, val)) + print(f" {key}: {val}") def main(): @@ -201,7 +181,7 @@ def main(): # print version information to the shell if args.version: - print("OpenTimelineIO version: {}".format(otio.__version__)) + print(f"OpenTimelineIO version: {otio.__version__}") if pkg_resources: pkg_resource_plugins = list( @@ -210,14 +190,14 @@ def main(): if pkg_resource_plugins: print("Plugins from pkg_resources:") for plugin in pkg_resource_plugins: - print(" {}".format(plugin.dist)) + print(f" {plugin.dist}") else: print("No pkg_resource plugins installed.") # list the loaded manifests print("Manifests loaded:") for mf in active_plugin_manifest.source_files: - print(" {}".format(mf)) + print(f" {mf}") for pt in plugin_types: # hooks have special code (see below) @@ -226,7 +206,7 @@ def main(): # header print("") - print("{}:".format(pt)) + print(f"{pt}:") # filter plugins by the patterns passed in on the command line plugin_by_type = getattr(active_plugin_manifest, pt) @@ -240,7 +220,7 @@ def main(): print(" (none found)") for plug in plugins: - print(" {}".format(plug.name)) + print(f" {plug.name}") try: info = plug.plugin_info_map() @@ -273,9 +253,9 @@ def main(): args.plugpattern ) for hookname in hooknames: - print(" {}".format(hookname)) + print(f" {hookname}") for hook_script in active_plugin_manifest.hooks[hookname]: - print(" {}".format(hook_script)) + print(f" {hook_script}") if not active_plugin_manifest.hooks[hookname]: print(" (no hook scripts attached)") diff --git a/src/py-opentimelineio/opentimelineio/console/otiostat.py b/src/py-opentimelineio/opentimelineio/console/otiostat.py index 7608ab61b..57f822f8c 100755 --- a/src/py-opentimelineio/opentimelineio/console/otiostat.py +++ b/src/py-opentimelineio/opentimelineio/console/otiostat.py @@ -1,27 +1,7 @@ #!/usr/bin/env python # +# SPDX-License-Identifier: Apache-2.0 # Copyright Contributors to the OpenTimelineIO project -# -# Licensed under the Apache License, Version 2.0 (the "Apache License") -# with the following modification; you may not use this file except in -# compliance with the Apache License and the following modification to it: -# Section 6. Trademarks. is deleted and replaced with: -# -# 6. Trademarks. This License does not grant permission to use the trade -# names, trademarks, service marks, or product names of the Licensor -# and its affiliates, except as required to comply with Section 4(c) of -# the License and to reproduce the content of the NOTICE file. -# -# You may obtain a copy of the Apache License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the Apache License with the above modification is -# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -# KIND, either express or implied. See the Apache License for the specific -# language governing permissions and limitations under the Apache License. -# """Print statistics about the otio file, including validation information.""" @@ -65,7 +45,7 @@ def _did_parse(input): @stat_check("top level object") def _top_level_object(input): - return "{}.{}".format(input.schema_name(), input.schema_version()) + return f"{input.schema_name()}.{input.schema_version()}" @stat_check("number of tracks") @@ -110,7 +90,7 @@ def depth(parent): @stat_check("number of clips") def _num_clips(input): - return len(list(input.each_clip())) + return len(list(input.find_clips())) @stat_check("total duration") @@ -140,7 +120,7 @@ def _top_level_rate(input): @stat_check("clips with cdl data") def _clips_with_cdl_data(input): - return len(list(c for c in input.each_clip() if 'cdl' in c.metadata)) + return len(list(c for c in input.find_clips() if 'cdl' in c.metadata)) @stat_check("Tracks with non standard types") @@ -148,7 +128,7 @@ def _sequences_with_non_standard_types(input): return len( list( c - for c in input.each_child(descended_from_type=otio.schema.Track) + for c in input.find_children(descended_from_type=otio.schema.Track) if c.kind not in (otio.schema.TrackKind.__dict__) ) ) @@ -157,7 +137,7 @@ def _sequences_with_non_standard_types(input): def _stat_otio(input_otio): for (test, testfunc) in TESTS: try: - print("{}: {}".format(test, testfunc(input_otio))) + print(f"{test}: {testfunc(input_otio)}") except (otio.exceptions.OTIOError) as e: sys.stderr.write( "There was an OTIO Error: " @@ -165,7 +145,7 @@ def _stat_otio(input_otio): ) continue except (Exception) as e: - sys.stderr.write("There was a system error: {}\n".format(e)) + sys.stderr.write(f"There was a system error: {e}\n") continue @@ -183,7 +163,7 @@ def main(): ) continue except (Exception) as e: - sys.stderr.write("There was a system error: {}\n".format(e)) + sys.stderr.write(f"There was a system error: {e}\n") continue _stat_otio(parsed_otio) diff --git a/src/py-opentimelineio/opentimelineio/console/otiotool.py b/src/py-opentimelineio/opentimelineio/console/otiotool.py new file mode 100755 index 000000000..f35af2e46 --- /dev/null +++ b/src/py-opentimelineio/opentimelineio/console/otiotool.py @@ -0,0 +1,867 @@ +#!/usr/bin/env python +# +# SPDX-License-Identifier: Apache-2.0 +# Copyright Contributors to the OpenTimelineIO project + +"""otiotool is a multipurpose command line tool for inspecting, +modifying, combining, and splitting OTIO files. + +Each of the many operations it can perform is provided by a +small, simple utility function. These functions also serve +as concise examples of how OTIO can be used to perform common +workflow tasks.""" + +import argparse +import os +import pathlib +import re +import sys + +from urllib.request import urlopen + +from copy import deepcopy + +import opentimelineio as otio + + +def main(): + """otiotool main program. + This function is resposible for executing the steps specified + by all of the command line arguments in the right order. + """ + + args = parse_arguments() + + # Special case option, which skips all the other phases + + if args.list_versions: + print("Available versions for --downgrade FAMILY:VERSION") + for family, mapping in otio.versioning.full_map().items(): + for label in mapping.keys(): + print(f" {family}:{label}") + return + + # Phase 1: Input... + + # Most of this function will operate on this list of timelines. + # Often there will be just one, but this tool in general enough + # to operate on several. This is essential when the --stack or + # --concatenate arguments are used. + if args.input: + timelines = read_inputs(args.input) + else: + timelines = [] + + # Phase 2: Filter (remove stuff)... + + if args.video_only: + for timeline in timelines: + keep_only_video_tracks(timeline) + + if args.audio_only: + for timeline in timelines: + keep_only_audio_tracks(timeline) + + if args.remove_transitions: + timelines = filter_transitions(timelines) + + if args.only_tracks_with_name or args.only_tracks_with_index: + timelines = filter_tracks( + args.only_tracks_with_name, + args.only_tracks_with_index, + timelines + ) + + if args.only_clips_with_name or args.only_clips_with_name_regex: + timelines = filter_clips( + args.only_clips_with_name, + args.only_clips_with_name_regex, + timelines + ) + + if args.trim: + for timeline in timelines: + trim_timeline(args.trim[0], args.trim[1], timeline) + + # Phase 3: Combine timelines + + if args.stack: + timelines = [stack_timelines(timelines)] + + if args.concat: + timelines = [concatenate_timelines(timelines)] + + # Phase 4: Combine (or add) tracks + + if args.flatten: + for timeline in timelines: + flatten_timeline( + timeline, + which_tracks=args.flatten, + keep=args.keep_flattened_tracks + ) + + # Phase 5: Relinking media + + if args.relink_by_name: + for timeline in timelines: + for folder in args.relink_by_name: + relink_by_name(timeline, folder) + + if args.copy_media_to_folder: + for timeline in timelines: + copy_media_to_folder(timeline, args.copy_media_to_folder) + + # Phase 6: Redaction + + if args.redact: + for timeline in timelines: + redact_timeline(timeline) + + # Phase 7: Inspection + + if args.stats: + for timeline in timelines: + print_timeline_stats(timeline) + + if args.inspect: + for timeline in timelines: + inspect_timelines(args.inspect, timeline) + + should_summarize = (args.list_clips or + args.list_media or + args.verify_media or + args.list_tracks or + args.list_markers) + if should_summarize: + for timeline in timelines: + summarize_timeline( + args.list_tracks, + args.list_clips, + args.list_media, + args.verify_media, + args.list_markers, + timeline) + + # Final Phase: Output + + if args.downgrade: + if ":" in args.downgrade: + label = args.downgrade + else: + label = "OTIO_CORE:" + args.downgrade + os.environ["OTIO_DEFAULT_TARGET_VERSION_FAMILY_LABEL"] = label + + if args.output: + # Gather all of the timelines under one OTIO object + # in preparation for final output + if len(timelines) > 1: + output = otio.schema.SerializableCollection() + output.extend(timelines) + else: + output = timelines[0] + + write_output(args.output, output) + + +def parse_arguments(): + parser = argparse.ArgumentParser( + description=""" +otiotool = a multi-purpose command line utility for working with OpenTimelineIO. + +This tool works in phases, as follows: +1. Input + Input files provided by the "--input " argument(s) are read into + memory. Files may be OTIO format, or any format supported by adapter + plugins. + +2. Filtering + Options such as --video-only, --audio-only, --only-tracks-with-name, + -only-tracks-with-index, --only-clips-with-name, + --only-clips-with-name-regex, --remove-transitions, and --trim will remove + content. Only the tracks, clips, etc. that pass all of the filtering options + provided are passed to the next phase. + +3. Combine + If specified, the --stack, --concat, and --flatten operations are + performed (in that order) to combine all of the input timeline(s) into one. + +4. Relink + The --relink-by-name option, will scan the specified folder(s) looking for + files which match the name of each clip in the input timeline(s). + If matching files are found, clips will be relinked to those files (using + file:// URLs). Clip names are matched to filenames ignoring file extension. + If specified, the --copy-media-to-folder option, will copy or download + all linked media, and relink the OTIO to reference the local copies. + +5. Redact + If specified, the --redact option, will remove all metadata and rename all + objects in the OTIO with generic names (e.g. "Track 1", "Clip 17", etc.) + +6. Inspect + Options such as --stats, --list-clips, --list-tracks, --list-media, + --verify-media, --list-markers, and --inspect will examine the OTIO and + print information to standard output. + +7. Output + Finally, if the "--output " option is specified, the resulting + OTIO will be written to the specified file. The extension of the output + filename is used to determine the format of the output (e.g. OTIO or any + format supported by the adapter plugins.) If you need to output an older + schema version, see the --downgrade option. +""".strip(), + epilog="""Examples: + +Combine multiple files into one, by joining them end-to-end: +otiotool -i titles.otio -i feature.otio -i credits.otio --concat -o full.otio + +Layer multiple files on top of each other in a stack: +otiotool -i background.otio -i foreground.otio --stack -o composite.otio + +Verify that all referenced media files are accessible: +otiotool -i playlist.otio --verify-media + +Inspect specific audio clips in detail: +otiotool -i playlist.otio --only-audio --list-tracks --inspect "Interview" +""", + formatter_class=argparse.RawDescriptionHelpFormatter + ) + + # Input... + parser.add_argument( + "-i", + "--input", + type=str, + nargs='+', + metavar='PATH(s)', + help="""Input file path(s). All formats supported by adapter plugins + are supported. Use '-' to read OTIO from standard input.""" + ) + + # Filter... + parser.add_argument( + "-v", + "--video-only", + action='store_true', + help="Output only video tracks" + ) + parser.add_argument( + "-a", + "--audio-only", + action='store_true', + help="Output only audio tracks" + ) + parser.add_argument( + "--only-tracks-with-name", + type=str, + nargs='+', + metavar='NAME(s)', + help="Output tracks with these name(s)" + ) + parser.add_argument( + "--only-tracks-with-index", + type=int, + nargs='+', + metavar='INDEX(es)', + help="Output tracks with these indexes" + " (1 based, in same order as --list-tracks)" + ) + parser.add_argument( + "--only-clips-with-name", + type=str, + nargs='+', + metavar='NAME(s)', + help="Output only clips with these name(s)" + ) + parser.add_argument( + "--only-clips-with-name-regex", + type=str, + nargs='+', + metavar='REGEX(es)', + help="Output only clips with names matching the given regex" + ) + parser.add_argument( + "--remove-transitions", + action='store_true', + help="Remove all transitions" + ) + parser.add_argument( + "-t", + "--trim", + type=str, + nargs=2, + metavar=('START', 'END'), + help="Trim from to as HH:MM:SS:FF timecode or seconds" + ) + + # Combine... + parser.add_argument( + "-f", + "--flatten", + choices=['video', 'audio', 'all'], + metavar='TYPE', + help="Flatten multiple tracks into one." + ) + parser.add_argument( + "--keep-flattened-tracks", + action='store_true', + help="""When used with --flatten, the new flat track is added above the + others instead of replacing them.""" + ) + parser.add_argument( + "-s", + "--stack", + action='store_true', + help="Stack multiple input files into one timeline" + ) + parser.add_argument( + "-c", + "--concat", + action='store_true', + help="Concatenate multiple input files end-to-end into one timeline" + ) + + # Relink + parser.add_argument( + "--relink-by-name", + type=str, + nargs='+', + metavar='FOLDER(s)', + help="""Scan the specified folder looking for filenames which match + each clip's name. If found, clips are relinked to those files.""" + ) + parser.add_argument( + "--copy-media-to-folder", + type=str, + metavar='FOLDER', + help="""Copy or download all linked media to the specified folder and + relink all media references to the copies""" + ) + + # Redact + parser.add_argument( + "--redact", + action='store_true', + help="""Remove all metadata, names, etc. leaving only the timeline + structure""" + ) + + # Inspect... + parser.add_argument( + "--stats", + action='store_true', + help="""List statistics about the result, including start, end, and + duration""" + ) + parser.add_argument( + "--list-clips", + action='store_true', + help="List each clip's name" + ) + parser.add_argument( + "--list-tracks", + action='store_true', + help="List each track's name" + ) + parser.add_argument( + "--list-media", + action='store_true', + help="List each referenced media URL" + ) + parser.add_argument( + "--verify-media", + action='store_true', + help="""Verify that each referenced media URL exists (for local media + only)""" + ) + parser.add_argument( + "--list-markers", + action='store_true', + help="List summary of all markers" + ) + parser.add_argument( + "--inspect", + type=str, + nargs='+', + metavar='NAME(s)', + help="Inspect details of clips with names matching the given regex" + ) + + # Output... + parser.add_argument( + "--downgrade", + type=str, + metavar='FAMILY:VERSION', + help="""Downgrade OTIO schema. Only relevant when --output is used + to output an OTIO file. FAMILY:VERSION specifies which schema family + and version to use. If FAMILY: is omitted, the default OTIO_CORE: is + used. For example `--downgrade OTIO_CORE:0.14.0` is equivalent to + `--downgrade 0.14.0`. See + https://opentimelineio.readthedocs.io/en/latest/tutorials/versioning-schemas.html + for details.""" + ) + parser.add_argument( + "--list-versions", + action='store_true', + help="""List available versions for the --downgrade option.""" + ) + + parser.add_argument( + "-o", + "--output", + type=str, + metavar='PATH', + help="""Output file. All formats supported by adapter plugins + are supported. Use '-' to write OTIO to standard output.""" + ) + + args = parser.parse_args() + + # At least one of these must be specified + if not any([args.input, args.list_versions]): + parser.error("Must specify at least one of --input or --list-versions.") + + # Some options cannot be combined. + + if args.video_only and args.audio_only: + parser.error("Cannot use --video-only and --audio-only at the same time.") + + if args.stack and args.concat: + parser.error("Cannot use --stack and --concat at the same time.") + + if args.keep_flattened_tracks and not args.flatten: + parser.error("Cannot use --keep-flattened-tracks without also using --flatten.") + + if args.input and args.list_versions: + parser.error("Cannot combine --input and --list-versions.") + + return args + + +def read_inputs(input_paths): + """Read one or more timlines from the list of file paths given. + If a file path is '-' then a timeline is read from stdin. + """ + timelines = [] + for input_path in input_paths: + if input_path == '-': + text = sys.stdin.read() + timeline = otio.adapters.read_from_string(text, 'otio_json') + else: + timeline = otio.adapters.read_from_file(input_path) + timelines.append(timeline) + return timelines + + +def keep_only_video_tracks(timeline): + """Remove all tracks except for video tracks from a timeline.""" + timeline.tracks[:] = timeline.video_tracks() + + +def keep_only_audio_tracks(timeline): + """Remove all tracks except for audio tracks from a timeline.""" + timeline.tracks[:] = timeline.audio_tracks() + + +def filter_transitions(timelines): + """Return a copy of the input timelines with all transitions removed. + The overall duration of the timelines should not be affected.""" + def _f(item): + if isinstance(item, otio.schema.Transition): + return None + return item + return [otio.algorithms.filtered_composition(t, _f) for t in timelines] + + +def _filter(item, names, patterns): + """This is a helper function that returns the input item if + its name matches the list of names given (if any), or matches any of the + patterns given (if any). If the item's name does not match any of the + given names or patterns, then None is returned.""" + if names and item.name in names: + return item + if patterns: + for pattern in patterns: + if re.search(pattern, item.name): + return item + return None + + # TODO: Should this return a same-duration Gap instead? + # gap = otio.schema.Gap(source_range=item.trimmed_range()) + # return gap + + +def filter_tracks(only_tracks_with_name, only_tracks_with_index, timelines): + """Return a copy of the input timelines with only tracks that match + either the list of names given, or the list of track indexes given.""" + + # Use a variable saved within this function so that the closure + # below can modify it. + # See: https://stackoverflow.com/questions/21959985/why-cant-python-increment-variable-in-closure # noqa: E501 + filter_tracks.index = 0 + + def _f(item): + if not isinstance(item, otio.schema.Track): + return item + filter_tracks.index = filter_tracks.index + 1 + if only_tracks_with_index and filter_tracks.index not in only_tracks_with_index: + return None + if only_tracks_with_name and item.name not in only_tracks_with_name: + return None + return item + + return [otio.algorithms.filtered_composition(t, _f) for t in timelines] + + +def filter_clips(only_clips_with_name, only_clips_with_name_regex, timelines): + """Return a copy of the input timelines with only clips with names + that match either the given list of names, or regular expression patterns.""" + + def _f(item): + if not isinstance(item, otio.schema.Clip): + return item + return _filter(item, only_clips_with_name, only_clips_with_name_regex) + + return [otio.algorithms.filtered_composition(t, _f) for t in timelines] + + +def stack_timelines(timelines): + """Return a single timeline with all of the tracks from all of the input + timelines stacked on top of each other. The resulting timeline should be + as long as the longest input timeline.""" + name = f"Stacked {len(timelines)} Timelines" + stacked_timeline = otio.schema.Timeline(name) + for timeline in timelines: + stacked_timeline.tracks.extend(deepcopy(timeline.tracks[:])) + return stacked_timeline + + +def concatenate_timelines(timelines): + """Return a single timeline with all of the input timelines concatenated + end-to-end. The resulting timeline should be as long as the sum of the + durations of the input timelines.""" + name = f"Concatenated {len(timelines)} Timelines" + concatenated_track = otio.schema.Track() + for timeline in timelines: + concatenated_track.append(deepcopy(timeline.tracks)) + concatenated_timeline = otio.schema.Timeline( + name=name, + tracks=[concatenated_track] + ) + return concatenated_timeline + + +def flatten_timeline(timeline, which_tracks='video', keep=False): + """Replace the tracks of this timeline with a single track by flattening. + If which_tracks is specified, you may choose 'video', 'audio', or 'all'. + If keep is True, then the old tracks are retained and the new one is added + above them instead of replacing them. This can be useful to see and + understand how flattening works.""" + + # Make two lists: tracks_to_flatten and other_tracks + # Note: that we take care to NOT assume that there are only two kinds + # of tracks. + if which_tracks == 'all': + tracks_to_flatten = timeline.tracks + other_tracks = [] + kind = tracks_to_flatten[0].kind + elif which_tracks == 'video': + tracks_to_flatten = timeline.video_tracks() + other_tracks = [t for t in timeline.tracks if t not in tracks_to_flatten] + kind = otio.schema.TrackKind.Video + elif which_tracks == 'audio': + tracks_to_flatten = timeline.audio_tracks() + other_tracks = [t for t in timeline.tracks if t not in tracks_to_flatten] + kind = otio.schema.TrackKind.Audio + else: + raise ValueError( + "Invalid choice {} for which_tracks argument" + " to flatten_timeline.".format(which_tracks) + ) + + flat_track = otio.algorithms.flatten_stack(tracks_to_flatten[:]) + flat_track.kind = kind + + if keep: + timeline.tracks.append(flat_track) + else: + timeline.tracks[:] = other_tracks + [flat_track] + + +def time_from_string(text, rate): + """This helper function turns a string into a RationalTime. It accepts + either a timecode string (e.g. "HH:MM:SS:FF") or a string with a floating + point value measured in seconds. The second argument to this function + specifies the rate for the returned RationalTime.""" + if ":" in text: + return otio.opentime.from_timecode(text, rate) + else: + return otio.opentime.from_seconds(float(text), rate) + + +def trim_timeline(start, end, timeline): + """Return a copy of the input timeline trimmed to the start and end + times given. Each of the start and end times can be specified as either + a timecode string (e.g. "HH:MM:SS:FF") or a string with a floating + point value measured in seconds.""" + if timeline.global_start_time is not None: + rate = timeline.global_start_time.rate + else: + rate = timeline.duration().rate + try: + start_time = time_from_string(start, rate) + end_time = time_from_string(end, rate) + except Exception: + raise ValueError("Start and end arguments to --trim must be " + "either HH:MM:SS:FF or a floating point number of" + " seconds, not '{}' and '{}'".format(start, end)) + trim_range = otio.opentime.range_from_start_end_time(start_time, end_time) + timeline.tracks[:] = [ + otio.algorithms.track_trimmed_to_range(t, trim_range) + for t in timeline.tracks + ] + + +# Used only within _counter() to keep track of object indexes +__counters = {} + + +def _counter(name): + """This is a helper function for returning redacted names, based on a name.""" + counter = __counters.get(name, 0) + counter += 1 + __counters[name] = counter + return counter + + +def redact_timeline(timeline): + """Remove all metadata, names, or other identifying information from this + timeline. Only the structure, schema and timing will remain.""" + + counter = _counter(timeline.schema_name()) + timeline.name = f"{timeline.schema_name()} #{counter}" + timeline.metadata.clear() + + for child in [timeline.tracks] + list(timeline.find_children()): + counter = _counter(child.schema_name()) + child.name = f"{child.schema_name()} #{counter}" + child.metadata.clear() + if hasattr(child, 'markers'): + for marker in child.markers: + counter = _counter(marker.schema_name()) + marker.name = f"{marker.schema_name()} #{counter}" + marker.metadata.clear() + if hasattr(child, 'effects'): + for effect in child.effects: + counter = _counter(effect.schema_name()) + effect.name = f"{effect.schema_name()} #{counter}" + effect.metadata.clear() + if hasattr(child, 'media_reference'): + media_reference = child.media_reference + if media_reference: + counter = _counter(media_reference.schema_name()) + has_target_url = hasattr(media_reference, 'target_url') + if has_target_url and media_reference.target_url: + media_reference.target_url = f"URL #{counter}" + media_reference.metadata.clear() + + +def copy_media(url, destination_path): + if url.startswith("/"): + print(f"COPYING: {url}") + data = open(url, "rb").read() + else: + print(f"DOWNLOADING: {url}") + data = urlopen(url).read() + open(destination_path, "wb").write(data) + return destination_path + + +def relink_by_name(timeline, path): + """Relink clips in the timeline to media files discovered at the + given folder path.""" + + def _conform_path(p): + # Turn absolute paths into file:// URIs + if os.path.isabs(p): + return pathlib.Path(p).as_uri() + else: + # Leave relative paths as-is + return p + + count = 0 + if os.path.isdir(path): + name_to_url = dict([ + ( + os.path.splitext(x)[0], + _conform_path(os.path.join(path, x)) + ) + for x in os.listdir(path) + ]) + elif os.path.isfile(path): + print((f"ERROR: Cannot relink to '{path}':" + " Please specify a folder instead of a file.")) + return + else: + print(f"ERROR: Cannot relink to '{path}': No such file or folder.") + return + + for clip in timeline.find_clips(): + url = name_to_url.get(clip.name) + if url is not None: + clip.media_reference = otio.schema.ExternalReference(target_url=url) + count += 1 + + print(f"Relinked {count} clips to files in folder {path}") + + +def copy_media_to_folder(timeline, folder): + """Copy or download all referenced media to this folder, and relink media + references to the copies.""" + + # @TODO: Add an option to allow mkdir + # if not os.path.exists(folder): + # os.mkdir(folder) + + copied_files = set() + for clip in timeline.find_clips(): + media_reference = clip.media_reference + has_actual_url = (media_reference and + hasattr(media_reference, 'target_url') and + media_reference.target_url) + if has_actual_url: + source_url = media_reference.target_url + filename = os.path.basename(source_url) + # @TODO: This is prone to name collisions if the basename is not unique + # We probably need to hash the url, or turn the whole url into a filename. + destination_path = os.path.join(folder, filename) + already_copied_this = destination_path in copied_files + file_exists = os.path.exists(destination_path) + if already_copied_this: + media_reference.target_url = destination_path + else: + if file_exists: + print( + "WARNING: Relinking clip {} to existing file" + " (instead of overwriting it): {}".format( + clip.name, destination_path + ) + ) + media_reference.target_url = destination_path + already_copied_this.add(destination_path) + else: + try: + copy_media(source_url, destination_path) + media_reference.target_url = destination_path + already_copied_this.add(destination_path) + except Exception as ex: + print(f"ERROR: Problem copying/downloading media {ex}") + # don't relink this one, since the copy failed + + +def print_timeline_stats(timeline): + """Print some statistics about the given timeline.""" + print(f"Name: {timeline.name}") + trimmed_range = timeline.tracks.trimmed_range() + print("Start: {}\nEnd: {}\nDuration: {}".format( + otio.opentime.to_timecode(trimmed_range.start_time), + otio.opentime.to_timecode(trimmed_range.end_time_exclusive()), + otio.opentime.to_timecode(trimmed_range.duration), + )) + + +def inspect_timelines(name_regex, timeline): + """Print some detailed information about the item(s) in the timeline with names + that match the given regular expression.""" + print("TIMELINE:", timeline.name) + items_to_inspect = [_filter(item, [], name_regex) + for item in timeline.find_children()] + items_to_inspect = list(filter(None, items_to_inspect)) + for item in items_to_inspect: + print(f" ITEM: {item.name} ({type(item)})") + print(" source_range:", item.source_range) + print(" trimmed_range:", item.trimmed_range()) + print(" visible_range:", item.visible_range()) + try: + print(" available_range:", item.available_range()) + except Exception: + pass + print(" range_in_parent:", item.range_in_parent()) + print( + " trimmed range in timeline:", + item.transformed_time_range( + item.trimmed_range(), timeline.tracks + ) + ) + print( + " visible range in timeline:", + item.transformed_time_range( + item.visible_range(), timeline.tracks + ) + ) + ancestor = item.parent() + while ancestor is not None: + print( + " range in {} ({}): {}".format( + ancestor.name, + type(ancestor), + item.transformed_time_range(item.trimmed_range(), ancestor) + ) + ) + ancestor = ancestor.parent() + + +def summarize_timeline(list_tracks, list_clips, list_media, verify_media, + list_markers, timeline): + """Print a summary of a timeline, optionally listing the tracks, clips, media, + and/or markers inside it.""" + print("TIMELINE:", timeline.name) + for child in [timeline.tracks] + list(timeline.find_children()): + if isinstance(child, otio.schema.Track): + if list_tracks: + print(f"TRACK: {child.name} ({child.kind})") + if isinstance(child, otio.schema.Clip): + if list_clips: + print(" CLIP:", child.name) + if list_media or verify_media: + try: + url = child.media_reference.target_url + except Exception: + url = None + detail = "" + if verify_media and url: + if os.path.exists(url): + detail = " EXISTS" + else: + detail = " NOT FOUND" + print(f" MEDIA{detail}: {url}") + + if list_markers and hasattr(child, 'markers'): + top_level = child + while top_level.parent() is not None: + top_level = top_level.parent() + for marker in child.markers: + template = " MARKER: global: {} local: {} duration: {} color: {} name: {}" # noqa: E501 + print(template.format( + otio.opentime.to_timecode(child.transformed_time( + marker.marked_range.start_time, + top_level)), + otio.opentime.to_timecode(marker.marked_range.start_time), + marker.marked_range.duration.value, + marker.color, + marker.name + )) + + +def write_output(output_path, output): + """Write the given OTIO object to a file path. If the file path given is + the string '-' then the output is written to stdout instead.""" + if output_path == '-': + result = otio.adapters.write_to_string(output) + print(result) + else: + otio.adapters.write_to_file(output, output_path) + + +if __name__ == '__main__': + main() diff --git a/src/py-opentimelineio/opentimelineio/core/__init__.py b/src/py-opentimelineio/opentimelineio/core/__init__.py index cab4aed57..5bd586cd4 100644 --- a/src/py-opentimelineio/opentimelineio/core/__init__.py +++ b/src/py-opentimelineio/opentimelineio/core/__init__.py @@ -1,3 +1,8 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright Contributors to the OpenTimelineIO project + +"""Core implementation details and wrappers around the C++ library""" + from .. _otio import ( # noqa # errors CannotComputeAvailableRangeError, @@ -19,15 +24,17 @@ instance_from_schema, register_serializable_object_type, register_upgrade_function, + register_downgrade_function, set_type_record, _serialize_json_to_string, _serialize_json_to_file, + type_version_map, + release_to_schema_version_map, ) from . _core_utils import ( # noqa add_method, _value_to_any, - _value_to_so_vector, _add_mutable_mapping_methods, _add_mutable_sequence_methods, ) @@ -38,16 +45,102 @@ item, ) +__all__ = [ + 'Composable', + 'Composition', + 'Item', + 'MediaReference', + 'SerializableObject', + 'SerializableObjectWithMetadata', + 'Track', + 'deserialize_json_from_file', + 'deserialize_json_from_string', + 'flatten_stack', + 'install_external_keepalive_monitor', + 'instance_from_schema', + 'set_type_record', + 'add_method', + 'upgrade_function_for', + 'downgrade_function_from', + 'serializable_field', + 'deprecated_field', + 'serialize_json_to_string', + 'serialize_json_to_file', + 'register_type', + 'type_version_map', + 'release_to_schema_version_map', +] + + +def serialize_json_to_string(root, schema_version_targets=None, indent=4): + """Serialize root to a json string. Optionally downgrade resulting schemas + to schema_version_targets. + + :param SerializableObject root: root object to serialize + :param dict[str, int] schema_version_targets: optional dictionary mapping + schema name to desired schema + version, for downgrading the + result to be compatible with + older versions of + OpenTimelineIO. + :param int indent: number of spaces for each json indentation level. Use -1 + for no indentation or newlines. + + :returns: resulting json string + :rtype: str + """ + return _serialize_json_to_string( + _value_to_any(root), + schema_version_targets or {}, + indent + ) + + +def serialize_json_to_file( + root, + filename, + schema_version_targets=None, + indent=4 +): + """Serialize root to a json file. Optionally downgrade resulting schemas + to schema_version_targets. + + :param SerializableObject root: root object to serialize + :param dict[str, int] schema_version_targets: optional dictionary mapping + schema name to desired schema + version, for downgrading the + result to be compatible with + older versions of + OpenTimelineIO. + :param int indent: number of spaces for each json indentation level. Use -1 + for no indentation or newlines. + + :returns: true for success, false for failure + :rtype: bool + """ + return _serialize_json_to_file( + _value_to_any(root), + filename, + schema_version_targets or {}, + indent + ) + -def serialize_json_to_string(root, indent=4): - return _serialize_json_to_string(_value_to_any(root), indent) +def register_type(classobj, schemaname=None): + """Decorator for registering a SerializableObject type + Example: -def serialize_json_to_file(root, filename, indent=4): - return _serialize_json_to_file(_value_to_any(root), filename, indent) + .. code-block:: python + @otio.core.register_type + class SimpleClass(otio.core.SerializableObject): + serializable_label = "SimpleClass.2" + ... -def register_type(classobj, schemaname=None): + :param typing.Type[SerializableObject] cls: class to register + :param str schemaname: Schema name (default: parse from serializable_label) + """ label = classobj._serializable_label if schemaname is None: schema_name, schema_version = label.split(".", 2) @@ -67,15 +160,19 @@ def __init__(self, *args, **kwargs): def upgrade_function_for(cls, version_to_upgrade_to): - """Decorator for identifying schema class upgrade functions. + """ + Decorator for identifying schema class upgrade functions. - Example - >>> @upgrade_function_for(MyClass, 5) - ... def upgrade_to_version_five(data): - ... pass + Example: - This will get called to upgrade a schema of MyClass to version 5. My class - must be a class deriving from otio.core.SerializableObject. + .. code-block:: python + + @upgrade_function_for(MyClass, 5) + def upgrade_to_version_five(data): + pass + + This will get called to upgrade a schema of MyClass to version 5. MyClass + must be a class deriving from :class:`~SerializableObject`. The upgrade function should take a single argument - the dictionary to upgrade, and return a dictionary with the fields upgraded. @@ -83,6 +180,9 @@ def upgrade_function_for(cls, version_to_upgrade_to): Remember that you don't need to provide an upgrade function for upgrades that add or remove fields, only for schema versions that change the field names. + + :param typing.Type[SerializableObject] cls: class to upgrade + :param int version_to_upgrade_to: the version to upgrade to """ def decorator_func(func): @@ -99,16 +199,55 @@ def wrapped_update(data): return decorator_func -def serializable_field(name, required_type=None, doc=None): - """Create a serializable_field for child classes of SerializableObject. +def downgrade_function_from(cls, version_to_downgrade_from): + """ + Decorator for identifying schema class downgrade functions. + + Example: + + .. code-block:: python + + @downgrade_function_from(MyClass, 5) + def downgrade_from_five_to_four(data): + return {"old_attr": data["new_attr"]} - Convienence function for adding attributes to child classes of - SerializableObject in such a way that they will be serialized/deserialized + This will get called to downgrade a schema of MyClass from version 5 to + version 4. MyClass must be a class deriving from + :class:`~SerializableObject`. + + The downgrade function should take a single argument - the dictionary to + downgrade, and return a dictionary with the fields downgraded. + + :param typing.Type[SerializableObject] cls: class to downgrade + :param int version_to_downgrade_from: the function downgrading from this + version to (version - 1) + """ + + def decorator_func(func): + """ Decorator for marking downgrade functions """ + def wrapped_update(data): + modified = func(data) + data.clear() + data.update(modified) + + register_downgrade_function( + cls._serializable_label.split(".")[0], + version_to_downgrade_from, + wrapped_update + ) + return func + + return decorator_func + + +def serializable_field(name, required_type=None, doc=None): + """ + Convenience function for adding attributes to child classes of + :class:`~SerializableObject` in such a way that they will be serialized/deserialized automatically. Use it like this: - .. highlight:: python .. code-block:: python @core.register_type @@ -117,7 +256,6 @@ class Foo(SerializableObject): This would indicate that class "foo" has a serializable field "bar". So: - .. highlight:: python .. code-block:: python f = foo() @@ -132,6 +270,13 @@ class Foo(SerializableObject): Additionally, the "doc" field will become the documentation for the property. + + :param str name: name of the field to add + :param type required_type: type required for the field + :param str doc: field documentation + + :return: property object + :rtype: :py:class:`property` """ def getter(self): @@ -155,7 +300,7 @@ def setter(self, val): def deprecated_field(): - """ For marking attributes on a SerializableObject deprecated. """ + """For marking attributes on a SerializableObject deprecated.""" def getter(self): raise DeprecationWarning diff --git a/src/py-opentimelineio/opentimelineio/core/_core_utils.py b/src/py-opentimelineio/opentimelineio/core/_core_utils.py index 3e35426ed..d58791a67 100644 --- a/src/py-opentimelineio/opentimelineio/core/_core_utils.py +++ b/src/py-opentimelineio/opentimelineio/core/_core_utils.py @@ -1,12 +1,9 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright Contributors to the OpenTimelineIO project + import types -try: - # Python 3.3+ - import collections.abc as collections_abc -except ImportError: - import collections as collections_abc +import collections.abc import copy -import collections -import sys from .. import ( _otio, @@ -33,41 +30,12 @@ ) -# XXX: python 2 vs python 3 guards -if sys.version_info[0] >= 3: - def _is_str(v): - return isinstance(v, str) - - def _iteritems(x): - return x.items() - - def _im_func(func): - return func - - def _xrange(*args): - return range(*args) - - _methodType = types.FunctionType -else: - # XXX Marked for no qa so that flake8 in python3 doesn't trip over these - # lines and falsely report them as bad. - def _is_str(v): - return isinstance(v, (str, unicode)) # noqa - - def _iteritems(x): - return x.items() - - def _im_func(func): - return func.im_func - - def _xrange(*args): - return xrange(*args) # noqa - - _methodType = types.MethodType +def _is_str(v): + return isinstance(v, str) def _is_nonstring_sequence(v): - return isinstance(v, collections_abc.Sequence) and not _is_str(v) + return isinstance(v, collections.abc.Sequence) and not _is_str(v) def _value_to_any(value, ids=None): @@ -76,13 +44,13 @@ def _value_to_any(value, ids=None): if isinstance(value, SerializableObject): return PyAny(value) - if isinstance(value, collections_abc.Mapping): + if isinstance(value, collections.abc.Mapping): if ids is None: ids = set() d = AnyDictionary() - for (k, v) in _iteritems(value): + for (k, v) in value.items(): if not _is_str(k): - raise ValueError("key '{}' is not a string".format(k)) + raise ValueError(f"key '{k}' is not a string") if id(v) in ids: raise ValueError( "circular reference converting dictionary to C++ datatype" @@ -121,8 +89,6 @@ def _value_to_any(value, ids=None): except RuntimeError: # communicate about integer range first biginttype = int - if sys.version_info[0] < 3: - biginttype = long # noqa: F821 if isinstance(value, biginttype): raise ValueError( "A value of {} is outside of the range of integers that " @@ -147,23 +113,6 @@ def _value_to_any(value, ids=None): ) -def _value_to_so_vector(value, ids=None): - if not isinstance(value, collections_abc.Sequence) or _is_str(value): - raise TypeError( - "Expected list/sequence of SerializableObjects;" - " found type '{}'".format(type(value)) - ) - - av = AnyVector() - for e in value: - if not isinstance(e, SerializableObject): - raise TypeError( - "Expected list/sequence of SerializableObjects;" - " found element of type '{}'".format(type(e))) - av.append(e) - return PyAny(av) - - _marker_ = object() @@ -197,26 +146,21 @@ def pop(self, key, default=_marker_): def __copy__(self): m = mapClass() - m.update(dict((k, v) for (k, v) in _iteritems(self))) + m.update({k: v for (k, v) in self.items()}) return m def __deepcopy__(self, memo): m = mapClass() - m.update( - dict( - (k, copy.deepcopy(v, memo)) - for (k, v) in _iteritems(self) - ) - ) + m.update({k: copy.deepcopy(v, memo) for (k, v) in self.items()}) return m - collections_abc.MutableMapping.register(mapClass) + collections.abc.MutableMapping.register(mapClass) mapClass.__setitem__ = __setitem__ mapClass.__str__ = __str__ mapClass.__repr__ = __repr__ seen = set() - for klass in (collections_abc.MutableMapping, collections_abc.Mapping): + for klass in (collections.abc.MutableMapping, collections.abc.Mapping): for name in klass.__dict__.keys(): if name in seen: continue @@ -224,10 +168,19 @@ def __deepcopy__(self, memo): seen.add(name) func = getattr(klass, name) if ( - isinstance(func, _methodType) + isinstance(func, types.FunctionType) and name not in klass.__abstractmethods__ ): - setattr(mapClass, name, _im_func(func)) + setattr(mapClass, name, func) + if name.startswith('__') or name.endswith('__'): # noqa + continue + + # Hide the method frm Sphinx doc. + # See https://www.sphinx-doc.org/en/master/usage/restructuredtext/domains.html#info-field-lists # noqa + # __doc__ will be None on a Windows embeddable package where the built-in modules are provided # noqa + # as pyc files which do not include the docstrings. + if getattr(mapClass, name).__doc__ is not None: + getattr(mapClass, name).__doc__ += "\n\n:meta private:" mapClass.setdefault = setdefault mapClass.pop = pop @@ -253,7 +206,7 @@ def __add__(self, other): return list(self) + list(other) else: raise TypeError( - "Cannot add types '{}' and '{}'".format(type(self), type(other)) + f"Cannot add types '{type(self)}' and '{type(other)}'" ) def __radd__(self, other): @@ -268,7 +221,7 @@ def __repr__(self): def __getitem__(self, index): if isinstance(index, slice): indices = index.indices(len(self)) - return [self.__internal_getitem__(i) for i in _xrange(*indices)] + return [self.__internal_getitem__(i) for i in range(*indices)] else: return self.__internal_getitem__(index) @@ -277,7 +230,7 @@ def __setitem__(self, index, item): if not isinstance(index, slice): self.__internal_setitem__(index, conversion_func(item)) else: - if not isinstance(item, collections_abc.Iterable): + if not isinstance(item, collections.abc.Iterable): raise TypeError("can only assign an iterable") indices = range(*index.indices(len(self))) @@ -285,7 +238,7 @@ def __setitem__(self, index, item): if index.step in (1, None): if ( not side_effecting_insertions - and isinstance(item, collections_abc.MutableSequence) + and isinstance(item, collections.abc.MutableSequence) and len(item) == len(indices) ): for i0, i in enumerate(indices): @@ -314,7 +267,7 @@ def __setitem__(self, index, item): self.extend(cached_items) raise e else: - if not isinstance(item, collections_abc.Sequence): + if not isinstance(item, collections.abc.Sequence): raise TypeError("can only assign a sequence") if len(item) != len(indices): raise ValueError( @@ -343,7 +296,7 @@ def __delitem__(self, index): if not isinstance(index, slice): self.__internal_delitem__(index) else: - for i in reversed(_xrange(*index.indices(len(self)))): + for i in reversed(range(*index.indices(len(self)))): self.__delitem__(i) def insert(self, index, item): @@ -352,7 +305,7 @@ def insert(self, index, item): if conversion_func else item ) - collections_abc.MutableSequence.register(sequenceClass) + collections.abc.MutableSequence.register(sequenceClass) sequenceClass.__radd__ = __radd__ sequenceClass.__add__ = __add__ sequenceClass.__getitem__ = __getitem__ @@ -363,16 +316,26 @@ def insert(self, index, item): sequenceClass.__repr__ = __repr__ seen = set() - for klass in (collections_abc.MutableSequence, collections_abc.Sequence): + for klass in (collections.abc.MutableSequence, collections.abc.Sequence): for name in klass.__dict__.keys(): if name not in seen: seen.add(name) func = getattr(klass, name) if ( - isinstance(func, _methodType) + isinstance(func, types.FunctionType) and name not in klass.__abstractmethods__ + and not hasattr(sequenceClass, name) ): - setattr(sequenceClass, name, _im_func(func)) + setattr(sequenceClass, name, func) + if name.startswith('__') or name.endswith('__'): + continue + + # Hide the method frm Sphinx doc. + # See https://www.sphinx-doc.org/en/master/usage/restructuredtext/domains.html#info-field-lists # noqa + # __doc__ will be None on a Windows embeddable package where the built-in modules are provided # noqa + # as pyc files which do not include the docstrings. + if getattr(sequenceClass, name).__doc__ is not None: + getattr(sequenceClass, name).__doc__ += "\n\n:meta private:" if not issubclass(sequenceClass, SerializableObject): def __copy__(self): diff --git a/src/py-opentimelineio/opentimelineio/core/composable.py b/src/py-opentimelineio/opentimelineio/core/composable.py index 9f2a81e3f..2eba50d84 100644 --- a/src/py-opentimelineio/opentimelineio/core/composable.py +++ b/src/py-opentimelineio/opentimelineio/core/composable.py @@ -1,3 +1,6 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright Contributors to the OpenTimelineIO project + from . _core_utils import add_method from .. import _otio diff --git a/src/py-opentimelineio/opentimelineio/core/composition.py b/src/py-opentimelineio/opentimelineio/core/composition.py index 899d7b8a4..3388f2216 100644 --- a/src/py-opentimelineio/opentimelineio/core/composition.py +++ b/src/py-opentimelineio/opentimelineio/core/composition.py @@ -1,3 +1,6 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright Contributors to the OpenTimelineIO project + from . _core_utils import add_method from .. import _otio @@ -30,28 +33,3 @@ def __repr__(self): repr(self.metadata) ) ) - - -@add_method(_otio.Composition) -def each_child( - self, - search_range=None, - descended_from_type=_otio.Composable, - shallow_search=False, -): - """ Generator that returns each child contained in the composition in - the order in which it is found. - - Note that this function is now deprecated, please consider using - children_if() instead. - - Arguments: - search_range: if specified, only children whose range overlaps with - the search range will be yielded. - descended_from_type: if specified, only children who are a - descendent of the descended_from_type will be yielded. - shallow_search: if True, will only search children of self, not - and not recurse into children of children. - """ - for child in self.children_if(descended_from_type, search_range, shallow_search): - yield child diff --git a/src/py-opentimelineio/opentimelineio/core/item.py b/src/py-opentimelineio/opentimelineio/core/item.py index 42d4a4792..99f32f039 100644 --- a/src/py-opentimelineio/opentimelineio/core/item.py +++ b/src/py-opentimelineio/opentimelineio/core/item.py @@ -1,3 +1,6 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright Contributors to the OpenTimelineIO project + from . _core_utils import add_method from .. import _otio diff --git a/src/py-opentimelineio/opentimelineio/core/mediaReference.py b/src/py-opentimelineio/opentimelineio/core/mediaReference.py index e3509a711..aec89d305 100644 --- a/src/py-opentimelineio/opentimelineio/core/mediaReference.py +++ b/src/py-opentimelineio/opentimelineio/core/mediaReference.py @@ -1,3 +1,6 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright Contributors to the OpenTimelineIO project + from . _core_utils import add_method from .. import _otio diff --git a/src/py-opentimelineio/opentimelineio/exceptions.py b/src/py-opentimelineio/opentimelineio/exceptions.py index 5244429ee..964777756 100644 --- a/src/py-opentimelineio/opentimelineio/exceptions.py +++ b/src/py-opentimelineio/opentimelineio/exceptions.py @@ -1,26 +1,5 @@ -# +# SPDX-License-Identifier: Apache-2.0 # Copyright Contributors to the OpenTimelineIO project -# -# Licensed under the Apache License, Version 2.0 (the "Apache License") -# with the following modification; you may not use this file except in -# compliance with the Apache License and the following modification to it: -# Section 6. Trademarks. is deleted and replaced with: -# -# 6. Trademarks. This License does not grant permission to use the trade -# names, trademarks, service marks, or product names of the Licensor -# and its affiliates, except as required to comply with Section 4(c) of -# the License and to reproduce the content of the NOTICE file. -# -# You may obtain a copy of the Apache License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the Apache License with the above modification is -# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -# KIND, either express or implied. See the Apache License for the specific -# language governing permissions and limitations under the Apache License. -# """Exception classes for OpenTimelineIO""" from . _otio import ( # noqa @@ -30,6 +9,25 @@ CannotComputeAvailableRangeError ) +__all__ = [ + 'OTIOError', + 'NotAChildError', + 'CannotComputeAvailableRangeError', + 'UnsupportedSchemaError', + 'CouldNotReadFileError', + 'NoKnownAdapterForExtensionError', + 'ReadingNotSupportedError', + 'WritingNotSupportedError', + 'NotSupportedError', + 'InvalidSerializableLabelError', + 'AdapterDoesntSupportFunctionError', + 'InstancingNotAllowedError', + 'TransitionFollowingATransitionError', + 'MisconfiguredPluginError', + 'CannotTrimTransitionsError', + 'NoDefaultMediaLinkerError' +] + class CouldNotReadFileError(OTIOError): pass @@ -77,3 +75,7 @@ class CannotTrimTransitionsError(OTIOError): class NoDefaultMediaLinkerError(OTIOError): pass + + +class InvalidEnvironmentVariableError(OTIOError): + pass diff --git a/src/py-opentimelineio/opentimelineio/hooks.py b/src/py-opentimelineio/opentimelineio/hooks.py index 1406532fa..af7836b75 100644 --- a/src/py-opentimelineio/opentimelineio/hooks.py +++ b/src/py-opentimelineio/opentimelineio/hooks.py @@ -1,26 +1,5 @@ -# +# SPDX-License-Identifier: Apache-2.0 # Copyright Contributors to the OpenTimelineIO project -# -# Licensed under the Apache License, Version 2.0 (the "Apache License") -# with the following modification; you may not use this file except in -# compliance with the Apache License and the following modification to it: -# Section 6. Trademarks. is deleted and replaced with: -# -# 6. Trademarks. This License does not grant permission to use the trade -# names, trademarks, service marks, or product names of the Licensor -# and its affiliates, except as required to comply with Section 4(c) of -# the License and to reproduce the content of the NOTICE file. -# -# You may obtain a copy of the Apache License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the Apache License with the above modification is -# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -# KIND, either express or implied. See the Apache License for the specific -# language governing permissions and limitations under the Apache License. -# from . import ( plugins, @@ -30,58 +9,70 @@ __doc__ = """ HookScripts are plugins that run at defined points ("Hooks"). -They expose a hook_function with signature: -hook_function :: otio.schema.Timeline, Dict -> otio.schema.Timeline +They expose a ``hook_function`` with signature: + +.. py:function:: hook_function(timeline: opentimelineio.schema.Timeline, optional_argument_dict: dict[str, Any]) -> opentimelineio.schema.Timeline # noqa + :noindex: + + Hook function signature Both hook scripts and the hooks they attach to are defined in the plugin manifest. -You can attach multiple hook scripts to a hook. They will be executed in list +Multiple scripts can be attached to a hook. They will be executed in list order, first to last. -They are defined by the manifests HookScripts and hooks areas. - ->>> -{ - "OTIO_SCHEMA" : "PluginManifest.1", - "hook_scripts" : [ - { - "OTIO_SCHEMA" : "HookScript.1", - "name" : "example hook", - "execution_scope" : "in process", - "filepath" : "example.py" - } - ], - "hooks" : { - "pre_adapter_write" : ["example hook"], - "post_adapter_read" : [] - } -} - -The 'hook_scripts' area loads the python modules with the 'hook_function's to -call in them. The 'hooks' area defines the hooks (and any associated -scripts). You can further query and modify these from python. - ->>> import opentimelineio as otio -... hook_list = otio.hooks.scripts_attached_to("some_hook") # -> ['a','b','c'] -... -... # to run the hook scripts: -... otio.hooks.run("some_hook", some_timeline, optional_argument_dict) - -This will pass (some_timeline, optional_argument_dict) to 'a', which will -a new timeline that will get passed into 'b' with optional_argument_dict, +They are defined by the manifests :class:`HookScript`\\s and hooks areas. + +.. code-block:: json + + { + "OTIO_SCHEMA" : "PluginManifest.1", + "hook_scripts" : [ + { + "OTIO_SCHEMA" : "HookScript.1", + "name" : "example hook", + "filepath" : "example.py" + } + ], + "hooks" : { + "pre_adapter_write" : ["example hook"], + "post_adapter_read" : [] + } + } + +The ``hook_scripts`` area loads the python modules with the ``hook_function``\\s to +call in them. The ``hooks`` area defines the hooks (and any associated +scripts). You can further query and modify these from python. + +.. code-block:: python + + import opentimelineio as otio + hook_list = otio.hooks.scripts_attached_to("some_hook") # -> ['a','b','c'] + + # to run the hook scripts: + otio.hooks.run("some_hook", some_timeline, optional_argument_dict) + +This will pass (some_timeline, optional_argument_dict) to ``a``, which will +a new timeline that will get passed into ``b`` with ``optional_argument_dict``, etc. -To Edit the order, change the order in the list: +To edit the order, change the order in the list: ->>> hook_list[0], hook_list[2] = hook_list[2], hook_list[0] -... print hook_list # ['c','b','a'] +.. code-block:: python -Now c will run, then b, then a. + hook_list[0], hook_list[2] = hook_list[2], hook_list[0] + print hook_list # ['c','b','a'] + +Now ``c`` will run, then ``b``, then ``a``. To delete a function the list: ->>> del hook_list[1] +.. code-block:: python + + del hook_list[1] + +---- """ @@ -92,12 +83,11 @@ class HookScript(plugins.PythonPlugin): def __init__( self, name=None, - execution_scope=None, filepath=None, ): """HookScript plugin constructor.""" - super(HookScript, self).__init__(name, execution_scope, filepath) + super().__init__(name, filepath) def run(self, in_timeline, argument_map={}): """Run the hook_function associated with this plugin.""" @@ -111,9 +101,8 @@ def run(self, in_timeline, argument_map={}): ) def __str__(self): - return "HookScript({}, {}, {})".format( + return "HookScript({}, {})".format( repr(self.name), - repr(self.execution_scope), repr(self.filepath) ) @@ -121,11 +110,9 @@ def __repr__(self): return ( "otio.hooks.HookScript(" "name={}, " - "execution_scope={}, " "filepath={}" ")".format( repr(self.name), - repr(self.execution_scope), repr(self.filepath) ) ) @@ -149,7 +136,7 @@ def available_hookscripts(): def scripts_attached_to(hook): - """Return an editable list of all the hook scriptss that are attached to + """Return an editable list of all the hook scripts that are attached to the specified hook, in execution order. Changing this list will change the order that scripts run in, and deleting a script will remove it from executing diff --git a/src/py-opentimelineio/opentimelineio/media_linker.py b/src/py-opentimelineio/opentimelineio/media_linker.py index 08d670ef5..41ae876ee 100644 --- a/src/py-opentimelineio/opentimelineio/media_linker.py +++ b/src/py-opentimelineio/opentimelineio/media_linker.py @@ -1,54 +1,20 @@ -# +# SPDX-License-Identifier: Apache-2.0 # Copyright Contributors to the OpenTimelineIO project -# -# Licensed under the Apache License, Version 2.0 (the "Apache License") -# with the following modification; you may not use this file except in -# compliance with the Apache License and the following modification to it: -# Section 6. Trademarks. is deleted and replaced with: -# -# 6. Trademarks. This License does not grant permission to use the trade -# names, trademarks, service marks, or product names of the Licensor -# and its affiliates, except as required to comply with Section 4(c) of -# the License and to reproduce the content of the NOTICE file. -# -# You may obtain a copy of the Apache License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the Apache License with the above modification is -# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -# KIND, either express or implied. See the Apache License for the specific -# language governing permissions and limitations under the Apache License. -# - -""" MediaLinker plugins fire after an adapter has read a file in order to -produce MediaReferences that point at valid, site specific media. - -They expose a "link_media_reference" function with the signature: -link_media_reference :: otio.schema.Clip -> otio.core.MediaReference - -or: - def linked_media_reference(from_clip): - result = otio.core.MediaReference() # whichever subclass - # do stuff - return result + +r""" +MediaLinker plugins fire after an adapter has read a file in order to +produce :class:`.MediaReference`\s that point at valid, site specific media. + +They expose a ``link_media_reference`` function with the signature: + +.. py:function:: link_media_reference(in_clip: opentimelineio.schema.Clip) -> opentimelineio.core.MediaReference # noqa + :noindex: + + Example link_media_reference function. To get context information, they can inspect the metadata on the clip and on -the media reference. The .parent() method can be used to find the containing +the media reference. The :meth:`.Composable.parent` method can be used to find the containing track if metadata is stored there. - -Please raise an instance (or child instance) of -otio.exceptions.CannotLinkMediaError() if there is a problem linking the media. - -For example: - for clip in timeline.each_clip(): - try: - new_mr = otio.media_linker.linked_media_reference(clip) - clip.media_reference = new_mr - except otio.exceptions.CannotLinkMediaError: - # or report the error - pass """ import os @@ -61,8 +27,8 @@ def linked_media_reference(from_clip): ) -# Enum describing different media linker policies class MediaLinkingPolicy: + """Enum describing different media linker policies""" DoNotLinkMedia = "__do_not_link_media" ForceDefaultLinker = "__default" @@ -135,10 +101,9 @@ class MediaLinker(plugins.PythonPlugin): def __init__( self, name=None, - execution_scope=None, filepath=None, ): - super(MediaLinker, self).__init__(name, execution_scope, filepath) + super().__init__(name, filepath) def link_media_reference(self, in_clip, media_linker_argument_map=None): media_linker_argument_map = media_linker_argument_map or {} @@ -155,7 +120,7 @@ def is_default_linker(self): def plugin_info_map(self): """Adds extra adapter-specific information to call to the parent fn.""" - result = super(MediaLinker, self).plugin_info_map() + result = super().plugin_info_map() fn_doc = inspect.getdoc(self.module().link_media_reference) if fn_doc: @@ -169,9 +134,8 @@ def plugin_info_map(self): return result def __str__(self): - return "MediaLinker({}, {}, {})".format( + return "MediaLinker({}, {})".format( repr(self.name), - repr(self.execution_scope), repr(self.filepath) ) @@ -179,11 +143,9 @@ def __repr__(self): return ( "otio.media_linker.MediaLinker(" "name={}, " - "execution_scope={}, " "filepath={}" ")".format( repr(self.name), - repr(self.execution_scope), repr(self.filepath) ) ) diff --git a/src/py-opentimelineio/opentimelineio/opentime.py b/src/py-opentimelineio/opentimelineio/opentime.py index 6bf9cad5a..f164935b7 100644 --- a/src/py-opentimelineio/opentimelineio/opentime.py +++ b/src/py-opentimelineio/opentimelineio/opentime.py @@ -1,9 +1,30 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright Contributors to the OpenTimelineIO project + from . _opentime import ( # noqa RationalTime, TimeRange, TimeTransform, ) +__all__ = [ + 'RationalTime', + 'TimeRange', + 'TimeTransform', + 'from_frames', + 'from_timecode', + 'from_time_string', + 'from_seconds', + 'to_timecode', + 'to_frames', + 'to_seconds', + 'to_time_string', + 'range_from_start_end_time', + 'range_from_start_end_time_inclusive', + 'duration_from_start_end_time', + 'duration_from_start_end_time_inclusive', +] + from_frames = RationalTime.from_frames from_timecode = RationalTime.from_timecode from_time_string = RationalTime.from_time_string @@ -18,6 +39,7 @@ def to_timecode(rt, rate=None, drop_frame=None): + """Convert a :class:`~RationalTime` into a timecode string.""" return ( rt.to_timecode() if rate is None and drop_frame is None @@ -26,12 +48,18 @@ def to_timecode(rt, rate=None, drop_frame=None): def to_frames(rt, rate=None): + """Turn a :class:`~RationalTime` into a frame number.""" return rt.to_frames() if rate is None else rt.to_frames(rate) def to_seconds(rt): + """Convert a :class:`~RationalTime` into float seconds""" return rt.to_seconds() def to_time_string(rt): + """ + Convert this timecode to time as used by ffmpeg, formatted as + ``hh:mm:ss`` where ss is an integer or decimal number. + """ return rt.to_time_string() diff --git a/src/py-opentimelineio/opentimelineio/plugins/__init__.py b/src/py-opentimelineio/opentimelineio/plugins/__init__.py index e5e67b38e..c8c0a1f55 100644 --- a/src/py-opentimelineio/opentimelineio/plugins/__init__.py +++ b/src/py-opentimelineio/opentimelineio/plugins/__init__.py @@ -1,26 +1,5 @@ -# +# SPDX-License-Identifier: Apache-2.0 # Copyright Contributors to the OpenTimelineIO project -# -# Licensed under the Apache License, Version 2.0 (the "Apache License") -# with the following modification; you may not use this file except in -# compliance with the Apache License and the following modification to it: -# Section 6. Trademarks. is deleted and replaced with: -# -# 6. Trademarks. This License does not grant permission to use the trade -# names, trademarks, service marks, or product names of the Licensor -# and its affiliates, except as required to comply with Section 4(c) of -# the License and to reproduce the content of the NOTICE file. -# -# You may obtain a copy of the Apache License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the Apache License with the above modification is -# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -# KIND, either express or implied. See the Apache License for the specific -# language governing permissions and limitations under the Apache License. -# """Plugin system for OTIO""" diff --git a/src/py-opentimelineio/opentimelineio/plugins/manifest.py b/src/py-opentimelineio/opentimelineio/plugins/manifest.py index 722df1636..07a3de821 100644 --- a/src/py-opentimelineio/opentimelineio/plugins/manifest.py +++ b/src/py-opentimelineio/opentimelineio/plugins/manifest.py @@ -1,44 +1,19 @@ -# +# SPDX-License-Identifier: Apache-2.0 # Copyright Contributors to the OpenTimelineIO project -# -# Licensed under the Apache License, Version 2.0 (the "Apache License") -# with the following modification; you may not use this file except in -# compliance with the Apache License and the following modification to it: -# Section 6. Trademarks. is deleted and replaced with: -# -# 6. Trademarks. This License does not grant permission to use the trade -# names, trademarks, service marks, or product names of the Licensor -# and its affiliates, except as required to comply with Section 4(c) of -# the License and to reproduce the content of the NOTICE file. -# -# You may obtain a copy of the Apache License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the Apache License with the above modification is -# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -# KIND, either express or implied. See the Apache License for the specific -# language governing permissions and limitations under the Apache License. -# """OTIO Python Plugin Manifest system: locates plugins to OTIO.""" +from importlib import resources import inspect import logging import os +from pathlib import Path -# In some circumstances pkg_resources has bad performance characteristics. -# Using the envirionment variable: $OTIO_DISABLE_PKG_RESOURCE_PLUGINS disables -# OpenTimelineIO's import and of use of the pkg_resources module. -if os.environ.get("OTIO_DISABLE_PKG_RESOURCE_PLUGINS", False): - pkg_resources = None -else: - try: - # on some python interpreters, pkg_resources is not available - import pkg_resources - except ImportError: - pkg_resources = None +try: + from importlib import metadata +except ImportError: + # For python 3.7 + import importlib_metadata as metadata from .. import ( core, @@ -53,11 +28,12 @@ 'schemadefs', 'hook_scripts', 'hooks', + 'version_manifests', ] def manifest_from_file(filepath): - """Read the .json file at filepath into a Manifest object.""" + """Read the .json file at filepath into a :py:class:`Manifest` object.""" result = core.deserialize_json_from_file(filepath) absfilepath = os.path.abspath(filepath) @@ -76,10 +52,10 @@ def manifest_from_string(input_string): stack = inspect.stack() if len(stack) > 1 and len(stack[1]) > 3: # filename function name - name = "{}:{}".format(stack[1][1], stack[1][3]) + name = f"{stack[1][1]}:{stack[1][3]}" # set the value in the manifest - src_string = "call to manifest_from_string() in {}".format(name) + src_string = f"call to manifest_from_string() in {name}" result.source_files.append(src_string) result._update_plugin_source(src_string) @@ -111,6 +87,8 @@ def __init__(self): self.hooks = {} self.hook_scripts = [] + self.version_manifests = {} + adapters = core.serializable_field( "adapters", type([]), @@ -136,6 +114,11 @@ def __init__(self): type([]), "Scripts that can be attached to hooks." ) + version_manifests = core.serializable_field( + "version_manifests", + type({}), + "Sets of versions to downgrade schemas to." + ) def extend(self, another_manifest): """ @@ -152,7 +135,17 @@ def extend(self, another_manifest): self.media_linkers.extend(another_manifest.media_linkers) self.hook_scripts.extend(another_manifest.hook_scripts) + for family, label_map in another_manifest.version_manifests.items(): + # because self.version_manifests is an AnyDictionary instead of a + # vanilla python dictionary, it does not support the .set_default() + # method. + if family not in self.version_manifests: + self.version_manifests[family] = {} + self.version_manifests[family].update(label_map) + for trigger_name, hooks in another_manifest.hooks.items(): + # because self.hooks is an AnyDictionary instead of a vanilla + # python dictionary, it does not support the .set_default() method. if trigger_name not in self.hooks: self.hooks[trigger_name] = [] self.hooks[trigger_name].extend(hooks) @@ -219,13 +212,14 @@ def schemadef_module_from_name(self, name): def load_manifest(): - """ Walk the plugin manifest discovery systems and accumulate manifests. + """Walk the plugin manifest discovery systems and accumulate manifests. The order of loading (and precedence) is: - 1. manifests specfied via the OTIO_PLUGIN_MANIFEST_PATH variable - 2. builtin plugin manifest - 3. contrib plugin manifest - 4. setuptools.pkg_resources based plugin manifests + + 1. Manifests specified via the :term:`OTIO_PLUGIN_MANIFEST_PATH` variable + 2. Entrypoint based plugin manifests + 3. Builtin plugin manifest + 4. Contrib plugin manifest """ result = Manifest() @@ -252,37 +246,14 @@ def load_manifest(): result.extend(manifest_from_file(json_path)) - # the builtin plugin manifest - builtin_manifest_path = os.path.join( - os.path.dirname(os.path.dirname(inspect.getsourcefile(core))), - "adapters", - "builtin_adapters.plugin_manifest.json" - ) - if os.path.abspath(builtin_manifest_path) not in result.source_files: - plugin_manifest = manifest_from_file(builtin_manifest_path) - result.extend(plugin_manifest) - - # the contrib plugin manifest (located in the opentimelineio_contrib package) - try: - import opentimelineio_contrib as otio_c - - contrib_manifest_path = os.path.join( - os.path.dirname(inspect.getsourcefile(otio_c)), - "adapters", - "contrib_adapters.plugin_manifest.json" - ) - if os.path.abspath(contrib_manifest_path) not in result.source_files: - contrib_manifest = manifest_from_file(contrib_manifest_path) - result.extend(contrib_manifest) - - except ImportError: - pass + if not os.environ.get("OTIO_DISABLE_ENTRYPOINTS_PLUGINS"): + try: + entry_points = metadata.entry_points(group='opentimelineio.plugins') + except TypeError: + # For python <= 3.9 + entry_points = metadata.entry_points().get('opentimelineio.plugins', []) - # setuptools.pkg_resources based plugins - if pkg_resources: - for plugin in pkg_resources.iter_entry_points( - "opentimelineio.plugins" - ): + for plugin in entry_points: plugin_name = plugin.name try: plugin_entry_point = plugin.load() @@ -304,45 +275,77 @@ def load_manifest(): plugin_manifest._update_plugin_source(manifest_path) except AttributeError: - if not pkg_resources.resource_exists( - plugin.module_name, - 'plugin_manifest.json' - ): - raise - - filepath = os.path.abspath( - pkg_resources.resource_filename( - plugin.module_name, - 'plugin_manifest.json' - ) - ) + name = plugin_entry_point.__name__ + + try: + filepath = resources.files(name) / "plugin_manifest.json" + except AttributeError: + # For python <= 3.7 + with resources.path(name, "plugin_manifest.json") as p: + filepath = Path(p) - if filepath in result.source_files: + if filepath.as_posix() in result.source_files: continue - manifest_stream = pkg_resources.resource_stream( - plugin.module_name, - 'plugin_manifest.json' - ) plugin_manifest = core.deserialize_json_from_string( - manifest_stream.read().decode('utf-8') + filepath.read_text() ) - manifest_stream.close() - - plugin_manifest._update_plugin_source(filepath) - plugin_manifest.source_files.append(filepath) + plugin_manifest._update_plugin_source(filepath.as_posix()) + plugin_manifest.source_files.append(filepath.as_posix()) - except Exception: + except Exception as e: logging.exception( - "could not load plugin: {}".format(plugin_name) + f"could not load plugin: {plugin_name}. Exception is: {e}" ) continue result.extend(plugin_manifest) else: - # XXX: Should we print some kind of warning that pkg_resources isn't - # available? - pass + logging.debug( + "OTIO_DISABLE_ENTRYPOINTS_PLUGINS is set. " + "Entry points plugings have been disabled." + ) + + # the builtin plugin manifest + try: + builtin_manifest_path = ( + resources.files("opentimelineio.adapters") + / "builtin_adapters.plugin_manifest.json" + ).as_posix() + except AttributeError: + # For python <= 3.7 + with resources.path( + "opentimelineio.adapters", + "builtin_adapters.plugin_manifest.json" + ) as p: + builtin_manifest_path = p.as_posix() + + if os.path.abspath(builtin_manifest_path) not in result.source_files: + plugin_manifest = manifest_from_file(builtin_manifest_path) + result.extend(plugin_manifest) + + # the contrib plugin manifest (located in the opentimelineio_contrib package) + try: + try: + contrib_manifest_path = ( + resources.files("opentimelineio_contrib.adapters") + / "contrib_adapters.plugin_manifest.json" + ).as_posix() + except AttributeError: + # For python <= 3.7 + with resources.path( + "opentimelineio_contrib.adapters", + "contrib_adapters.plugin_manifest.json" + ) as p: + contrib_manifest_path = p.as_posix() + + except ModuleNotFoundError: + logging.debug("no opentimelineio_contrib.adapters package found") + + else: + if os.path.abspath(contrib_manifest_path) not in result.source_files: + contrib_manifest = manifest_from_file(contrib_manifest_path) + result.extend(contrib_manifest) # force the schemadefs to load and add to schemadef module namespace for s in result.schemadefs: diff --git a/src/py-opentimelineio/opentimelineio/plugins/python_plugin.py b/src/py-opentimelineio/opentimelineio/plugins/python_plugin.py index 2bcbd85f9..7a5d3be27 100644 --- a/src/py-opentimelineio/opentimelineio/plugins/python_plugin.py +++ b/src/py-opentimelineio/opentimelineio/plugins/python_plugin.py @@ -1,26 +1,5 @@ -# +# SPDX-License-Identifier: Apache-2.0 # Copyright Contributors to the OpenTimelineIO project -# -# Licensed under the Apache License, Version 2.0 (the "Apache License") -# with the following modification; you may not use this file except in -# compliance with the Apache License and the following modification to it: -# Section 6. Trademarks. is deleted and replaced with: -# -# 6. Trademarks. This License does not grant permission to use the trade -# names, trademarks, service marks, or product names of the Licensor -# and its affiliates, except as required to comply with Section 4(c) of -# the License and to reproduce the content of the NOTICE file. -# -# You may obtain a copy of the Apache License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the Apache License with the above modification is -# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -# KIND, either express or implied. See the Apache License for the specific -# language governing permissions and limitations under the Apache License. -# """Base class for OTIO plugins that are exposed by manifests.""" @@ -77,26 +56,15 @@ class PythonPlugin(core.SerializableObject): def __init__( self, name=None, - execution_scope=None, filepath=None, ): core.SerializableObject.__init__(self) self.name = name - self.execution_scope = execution_scope self.filepath = filepath self._json_path = None self._module = None name = core.serializable_field("name", doc="Adapter name.") - execution_scope = core.serializable_field( - "execution_scope", - str, - doc=( - "Describes whether this adapter is executed in the current python" - " process or in a subshell. Options are: " - "['in process', 'out of process']." - ) - ) filepath = core.serializable_field( "filepath", str, @@ -147,7 +115,7 @@ def _imported_module(self, namespace): with file_obj: # this will reload the module if it has already been loaded. mod = imp.load_module( - "opentimelineio.{}.{}".format(namespace, self.name), + f"opentimelineio.{namespace}.{self.name}", file_obj, pathname, description @@ -169,6 +137,6 @@ def _execute_function(self, func_name, **kwargs): # collects the error handling into a common place. if not hasattr(self.module(), func_name): raise exceptions.AdapterDoesntSupportFunctionError( - "Sorry, {} doesn't support {}.".format(self.name, func_name) + f"Sorry, {self.name} doesn't support {func_name}." ) return (getattr(self.module(), func_name)(**kwargs)) diff --git a/src/py-opentimelineio/opentimelineio/schema/__init__.py b/src/py-opentimelineio/opentimelineio/schema/__init__.py index 8079d278d..c7fc31bfc 100644 --- a/src/py-opentimelineio/opentimelineio/schema/__init__.py +++ b/src/py-opentimelineio/opentimelineio/schema/__init__.py @@ -1,26 +1,5 @@ -# +# SPDX-License-Identifier: Apache-2.0 # Copyright Contributors to the OpenTimelineIO project -# -# Licensed under the Apache License, Version 2.0 (the "Apache License") -# with the following modification; you may not use this file except in -# compliance with the Apache License and the following modification to it: -# Section 6. Trademarks. is deleted and replaced with: -# -# 6. Trademarks. This License does not grant permission to use the trade -# names, trademarks, service marks, or product names of the Licensor -# and its affiliates, except as required to comply with Section 4(c) of -# the License and to reproduce the content of the NOTICE file. -# -# You may obtain a copy of the Apache License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the Apache License with the above modification is -# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -# KIND, either express or implied. See the Apache License for the specific -# language governing permissions and limitations under the Apache License. -# # flake8: noqa @@ -65,17 +44,35 @@ image_sequence_reference, marker, serializable_collection, - stack, timeline, - track, transition, v2d, ) -track.TrackKind = TrackKind - def timeline_from_clips(clips): """Convenience for making a single track timeline from a list of clips.""" trck = Track(children=clips) return Timeline(tracks=[trck]) + +__all__ = [ + 'Box2d', + 'Clip', + 'Effect', + 'TimeEffect', + 'LinearTimeWarp', + 'ExternalReference', + 'FreezeFrame', + 'Gap', + 'GeneratorReference', + 'ImageSequenceReference', + 'Marker', + 'MissingReference', + 'SerializableCollection', + 'Stack', + 'Timeline', + 'Transition', + 'SchemaDef', + 'timeline_from_clips', + 'V2d' +] diff --git a/src/py-opentimelineio/opentimelineio/schema/box2d.py b/src/py-opentimelineio/opentimelineio/schema/box2d.py index 96df41fbc..d8937958d 100644 --- a/src/py-opentimelineio/opentimelineio/schema/box2d.py +++ b/src/py-opentimelineio/opentimelineio/schema/box2d.py @@ -1,3 +1,6 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright Contributors to the OpenTimelineIO project + from .. core._core_utils import add_method from .. import _otio diff --git a/src/py-opentimelineio/opentimelineio/schema/clip.py b/src/py-opentimelineio/opentimelineio/schema/clip.py index c64d90993..c0de97158 100644 --- a/src/py-opentimelineio/opentimelineio/schema/clip.py +++ b/src/py-opentimelineio/opentimelineio/schema/clip.py @@ -1,3 +1,6 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright Contributors to the OpenTimelineIO project + from .. core._core_utils import add_method from .. import _otio @@ -30,5 +33,5 @@ def __repr__(self): @add_method(_otio.Clip) -def each_clip(self, search_range=None): +def find_clips(self, search_range=None): yield self diff --git a/src/py-opentimelineio/opentimelineio/schema/effect.py b/src/py-opentimelineio/opentimelineio/schema/effect.py index 7f98235aa..3fc575ce3 100644 --- a/src/py-opentimelineio/opentimelineio/schema/effect.py +++ b/src/py-opentimelineio/opentimelineio/schema/effect.py @@ -1,3 +1,6 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright Contributors to the OpenTimelineIO project + from .. core._core_utils import add_method from .. import _otio diff --git a/src/py-opentimelineio/opentimelineio/schema/external_reference.py b/src/py-opentimelineio/opentimelineio/schema/external_reference.py index 405d69045..035369274 100644 --- a/src/py-opentimelineio/opentimelineio/schema/external_reference.py +++ b/src/py-opentimelineio/opentimelineio/schema/external_reference.py @@ -1,10 +1,13 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright Contributors to the OpenTimelineIO project + from .. core._core_utils import add_method from .. import _otio @add_method(_otio.ExternalReference) def __str__(self): - return 'ExternalReference("{}")'.format(str(self.target_url)) + return f'ExternalReference("{str(self.target_url)}")' @add_method(_otio.ExternalReference) diff --git a/src/py-opentimelineio/opentimelineio/schema/foo.py b/src/py-opentimelineio/opentimelineio/schema/foo.py deleted file mode 100644 index 6e42b8e1e..000000000 --- a/src/py-opentimelineio/opentimelineio/schema/foo.py +++ /dev/null @@ -1,17 +0,0 @@ -from .. import core - -"""Test Class""" - - -@core.register_type -class Foo(core.SerializableObjectWithMetadata): - _serializable_label = "Foo.1" - - def __init__(self, name="", metadata=None): - core.SerializableObjectWithMetadata.__init__(self, name, metadata) - - abc = core.serializable_field( - "abc", - type([]), - "an int" - ) diff --git a/src/py-opentimelineio/opentimelineio/schema/generator_reference.py b/src/py-opentimelineio/opentimelineio/schema/generator_reference.py index 4f89b62bf..418f4581d 100644 --- a/src/py-opentimelineio/opentimelineio/schema/generator_reference.py +++ b/src/py-opentimelineio/opentimelineio/schema/generator_reference.py @@ -1,3 +1,6 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright Contributors to the OpenTimelineIO project + from .. core._core_utils import add_method from .. import _otio diff --git a/src/py-opentimelineio/opentimelineio/schema/image_sequence_reference.py b/src/py-opentimelineio/opentimelineio/schema/image_sequence_reference.py index 5511184cd..f9e14bd12 100644 --- a/src/py-opentimelineio/opentimelineio/schema/image_sequence_reference.py +++ b/src/py-opentimelineio/opentimelineio/schema/image_sequence_reference.py @@ -1,3 +1,6 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright Contributors to the OpenTimelineIO project + from .. core._core_utils import add_method from .. import _otio @@ -55,12 +58,11 @@ def __repr__(self): @add_method(_otio.ImageSequenceReference) def frame_range_for_time_range(self, time_range): - """ - Returns a :class:`tuple` containing the first and last frame numbers for + """Returns first and last frame numbers for the given time range in the reference. - Raises ValueError if the provided time range is outside the available - range. + :rtype: tuple[int] + :raises ValueError: if the provided time range is outside the available range. """ return ( self.frame_for_time(time_range.start_time), @@ -71,7 +73,7 @@ def frame_range_for_time_range(self, time_range): @add_method(_otio.ImageSequenceReference) def abstract_target_url(self, symbol): """ - Generates a target url for a frame where :param:``symbol`` is used in place + Generates a target url for a frame where ``symbol`` is used in place of the frame number. This is often used to generate wildcard target urls. """ if not self.target_url_base.endswith("/"): diff --git a/src/py-opentimelineio/opentimelineio/schema/marker.py b/src/py-opentimelineio/opentimelineio/schema/marker.py index ec0525fd4..af1809018 100644 --- a/src/py-opentimelineio/opentimelineio/schema/marker.py +++ b/src/py-opentimelineio/opentimelineio/schema/marker.py @@ -1,3 +1,6 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright Contributors to the OpenTimelineIO project + from .. core._core_utils import add_method from .. import _otio diff --git a/src/py-opentimelineio/opentimelineio/schema/schemadef.py b/src/py-opentimelineio/opentimelineio/schema/schemadef.py index 38a4cfa40..21c389399 100644 --- a/src/py-opentimelineio/opentimelineio/schema/schemadef.py +++ b/src/py-opentimelineio/opentimelineio/schema/schemadef.py @@ -1,3 +1,6 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright Contributors to the OpenTimelineIO project + import inspect import collections @@ -16,17 +19,17 @@ class SchemaDef(plugins.PythonPlugin): def __init__( self, name=None, - execution_scope=None, filepath=None, ): - plugins.PythonPlugin.__init__(self, name, execution_scope, filepath) + plugins.PythonPlugin.__init__(self, name, filepath) def module(self): """ Return the module object for this schemadef plugin. If the module hasn't already been imported, it is imported and injected into the otio.schemadefs namespace as a side-effect. - (redefines PythonPlugin.module()) + + Redefines :py:meth:`.PythonPlugin.module`. """ if not self._module: @@ -40,7 +43,7 @@ def plugin_info_map(self): """Adds extra schemadef-specific information to call to the parent fn. """ - result = super(SchemaDef, self).plugin_info_map() + result = super().plugin_info_map() features = collections.OrderedDict() for name in dir(self.module()): @@ -62,9 +65,8 @@ def plugin_info_map(self): return result def __str__(self): - return "SchemaDef({}, {}, {})".format( + return "SchemaDef({}, {})".format( repr(self.name), - repr(self.execution_scope), repr(self.filepath) ) @@ -72,11 +74,9 @@ def __repr__(self): return ( "otio.schema.SchemaDef(" "name={}, " - "execution_scope={}, " "filepath={}" ")".format( repr(self.name), - repr(self.execution_scope), repr(self.filepath) ) ) diff --git a/src/py-opentimelineio/opentimelineio/schema/serializable_collection.py b/src/py-opentimelineio/opentimelineio/schema/serializable_collection.py index 7e6c2dbed..eed270941 100644 --- a/src/py-opentimelineio/opentimelineio/schema/serializable_collection.py +++ b/src/py-opentimelineio/opentimelineio/schema/serializable_collection.py @@ -1,3 +1,6 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright Contributors to the OpenTimelineIO project + from .. core._core_utils import add_method from .. import _otio @@ -25,37 +28,3 @@ def __repr__(self): repr(self.metadata) ) ) - - -@add_method(_otio.SerializableCollection) -def each_child(self, search_range=None, descended_from_type=_otio.Composable): - """ Generator that returns each child contained in the serializable - collection in the order in which it is found. - - Note that this function is now deprecated, please consider using - children_if() instead. - - Arguments: - search_range: if specified, only children whose range overlaps with - the search range will be yielded. - descended_from_type: if specified, only children who are a - descendent of the descended_from_type will be yielded. - """ - for child in self.children_if(descended_from_type, search_range): - yield child - - -@add_method(_otio.SerializableCollection) -def each_clip(self, search_range=None): - """ Generator that returns each clip contained in the serializable - collection in the order in which it is found. - - Note that this function is now deprecated, please consider using - clip_if() instead. - - Arguments: - search_range: if specified, only children whose range overlaps with - the search range will be yielded. - """ - for child in self.clip_if(search_range): - yield child diff --git a/src/py-opentimelineio/opentimelineio/schema/stack.py b/src/py-opentimelineio/opentimelineio/schema/stack.py deleted file mode 100644 index 905a92e9b..000000000 --- a/src/py-opentimelineio/opentimelineio/schema/stack.py +++ /dev/null @@ -1,18 +0,0 @@ -from .. core._core_utils import add_method -from .. import _otio - - -@add_method(_otio.Stack) -def each_clip(self, search_range=None): - """ Generator that returns each clip contained in the stack - in the order in which it is found. - - Note that this function is now deprecated, please consider using - clip_if() instead. - - Arguments: - search_range: if specified, only children whose range overlaps with - the search range will be yielded. - """ - for child in self.clip_if(search_range): - yield child diff --git a/src/py-opentimelineio/opentimelineio/schema/timeline.py b/src/py-opentimelineio/opentimelineio/schema/timeline.py index 24f9d1d6e..c6185910d 100644 --- a/src/py-opentimelineio/opentimelineio/schema/timeline.py +++ b/src/py-opentimelineio/opentimelineio/schema/timeline.py @@ -1,10 +1,13 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright Contributors to the OpenTimelineIO project + from .. core._core_utils import add_method from .. import _otio @add_method(_otio.Timeline) def __str__(self): - return 'Timeline("{}", {})'.format(str(self.name), str(self.tracks)) + return f'Timeline("{str(self.name)}", {str(self.tracks)})' @add_method(_otio.Timeline) @@ -15,37 +18,3 @@ def __repr__(self): repr(self.tracks) ) ) - - -@add_method(_otio.Timeline) -def each_child(self, search_range=None, descended_from_type=_otio.Composable): - """ Generator that returns each child contained in the timeline - in the order in which it is found. - - Note that this function is now deprecated, please consider using - children_if() instead. - - Arguments: - search_range: if specified, only children whose range overlaps with - the search range will be yielded. - descended_from_type: if specified, only children who are a - descendent of the descended_from_type will be yielded. - """ - for child in self.children_if(descended_from_type, search_range): - yield child - - -@add_method(_otio.Timeline) -def each_clip(self, search_range=None): - """ Generator that returns each clip contained in the timeline - in the order in which it is found. - - Note that this function is now deprecated, please consider using - clip_if() instead. - - Arguments: - search_range: if specified, only children whose range overlaps with - the search range will be yielded. - """ - for child in self.clip_if(search_range): - yield child diff --git a/src/py-opentimelineio/opentimelineio/schema/track.py b/src/py-opentimelineio/opentimelineio/schema/track.py deleted file mode 100644 index b0ebbb60b..000000000 --- a/src/py-opentimelineio/opentimelineio/schema/track.py +++ /dev/null @@ -1,20 +0,0 @@ -from .. core._core_utils import add_method -from .. import _otio - - -@add_method(_otio.Track) -def each_clip(self, search_range=None, shallow_search=False): - """ Generator that returns each clip contained in the track - in the order in which it is found. - - Note that this function is now deprecated, please consider using - clip_if() instead. - - Arguments: - search_range: if specified, only children whose range overlaps with - the search range will be yielded. - shallow_search: if True, will only search children of self, not - and not recurse into children of children. - """ - for child in self.clip_if(search_range): - yield child diff --git a/src/py-opentimelineio/opentimelineio/schema/transition.py b/src/py-opentimelineio/opentimelineio/schema/transition.py index 54c68ac05..a344b61ae 100644 --- a/src/py-opentimelineio/opentimelineio/schema/transition.py +++ b/src/py-opentimelineio/opentimelineio/schema/transition.py @@ -1,3 +1,6 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright Contributors to the OpenTimelineIO project + from .. core._core_utils import add_method from .. import _otio diff --git a/src/py-opentimelineio/opentimelineio/schema/v2d.py b/src/py-opentimelineio/opentimelineio/schema/v2d.py index 34f7fd68b..ce8fcea1d 100644 --- a/src/py-opentimelineio/opentimelineio/schema/v2d.py +++ b/src/py-opentimelineio/opentimelineio/schema/v2d.py @@ -1,3 +1,6 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright Contributors to the OpenTimelineIO project + from .. core._core_utils import add_method from .. import _otio diff --git a/src/py-opentimelineio/opentimelineio/schemadef/__init__.py b/src/py-opentimelineio/opentimelineio/schemadef/__init__.py index 568b3eaaa..8efdee9ab 100644 --- a/src/py-opentimelineio/opentimelineio/schemadef/__init__.py +++ b/src/py-opentimelineio/opentimelineio/schemadef/__init__.py @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright Contributors to the OpenTimelineIO project def _add_schemadef_module(name, mod): """Insert a new module name and module object into schemadef namespace.""" diff --git a/src/py-opentimelineio/opentimelineio/test_utils.py b/src/py-opentimelineio/opentimelineio/test_utils.py index eff57c02a..f4483fef5 100644 --- a/src/py-opentimelineio/opentimelineio/test_utils.py +++ b/src/py-opentimelineio/opentimelineio/test_utils.py @@ -1,27 +1,7 @@ #!/usr/bin/env python # +# SPDX-License-Identifier: Apache-2.0 # Copyright Contributors to the OpenTimelineIO project -# -# Licensed under the Apache License, Version 2.0 (the "Apache License") -# with the following modification; you may not use this file except in -# compliance with the Apache License and the following modification to it: -# Section 6. Trademarks. is deleted and replaced with: -# -# 6. Trademarks. This License does not grant permission to use the trade -# names, trademarks, service marks, or product names of the Licensor -# and its affiliates, except as required to comply with Section 4(c) of -# the License and to reproduce the content of the NOTICE file. -# -# You may obtain a copy of the Apache License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the Apache License with the above modification is -# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -# KIND, either express or implied. See the Apache License for the specific -# language governing permissions and limitations under the Apache License. -# """Utility assertions for OTIO Unit tests.""" @@ -32,7 +12,7 @@ ) -class OTIOAssertions(object): +class OTIOAssertions: def assertJsonEqual(self, known, test_result): """Convert to json and compare that (more readable).""" self.maxDiff = None diff --git a/src/py-opentimelineio/opentimelineio/url_utils.py b/src/py-opentimelineio/opentimelineio/url_utils.py index bc0fada75..9e1c02520 100644 --- a/src/py-opentimelineio/opentimelineio/url_utils.py +++ b/src/py-opentimelineio/opentimelineio/url_utils.py @@ -1,48 +1,19 @@ -# +# SPDX-License-Identifier: Apache-2.0 # Copyright Contributors to the OpenTimelineIO project -# -# Licensed under the Apache License, Version 2.0 (the "Apache License") -# with the following modification; you may not use this file except in -# compliance with the Apache License and the following modification to it: -# Section 6. Trademarks. is deleted and replaced with: -# -# 6. Trademarks. This License does not grant permission to use the trade -# names, trademarks, service marks, or product names of the Licensor -# and its affiliates, except as required to comply with Section 4(c) of -# the License and to reproduce the content of the NOTICE file. -# -# You may obtain a copy of the Apache License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the Apache License with the above modification is -# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -# KIND, either express or implied. See the Apache License for the specific -# language governing permissions and limitations under the Apache License. -# """Utilities for conversion between urls and file paths""" import os -try: - # Python 2.7 - import urlparse - import urllib -except ImportError: - # Python 3 - import urllib.parse as urlparse - from urllib import request as urllib - -try: - import pathlib -except ImportError: - import pathlib2 as pathlib +from urllib import ( + parse as urlparse, + request +) +import pathlib def url_from_filepath(fpath): - """convert a filesystem path to an url in a portable way using / path sep""" + """Convert a filesystem path to an url in a portable way using / path sep""" try: # appears to handle absolute windows paths better, which are absolute @@ -67,5 +38,6 @@ def url_from_filepath(fpath): def filepath_from_url(urlstr): """ Take a url and return a filepath """ + parsed_result = urlparse.urlparse(urlstr) - return urllib.url2pathname(parsed_result.path) + return request.url2pathname(parsed_result.path) diff --git a/src/py-opentimelineio/opentimelineio/versioning.py b/src/py-opentimelineio/opentimelineio/versioning.py new file mode 100644 index 000000000..4fd0e9126 --- /dev/null +++ b/src/py-opentimelineio/opentimelineio/versioning.py @@ -0,0 +1,89 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright Contributors to the OpenTimelineIO project + +"""Tools for fetching the family->label->schema:version maps""" + +import copy + +from . import ( + core, + plugins +) + + +def full_map(): + """Return the full map of schema version sets, including core and plugins. + Organized as follows: + + .. code-block:: python + + { + "FAMILY_NAME": { + "LABEL": { + "SchemaName": schemaversion, + "Clip": 2, + "Timeline": 3, + ... + } + } + } + + + The "OTIO_CORE" family is always provided and represents the built in + schemas defined in the C++ core. + IE: + + .. code-block:: python + + { + "OTIO_CORE": { + "0.15.0": { + "Clip": 2, + ... + } + } + } + + :returns: full map of schema version sets, including core and plugins + :rtype: dict[str, dict[str, dict[str, int]]] + """ + + result = copy.deepcopy(plugins.ActiveManifest().version_manifests) + result.update( + { + "OTIO_CORE": core.release_to_schema_version_map(), + } + ) + return result + + +def fetch_map(family, label): + """Fetch the version map for the given family and label. OpenTimelineIO + includes a built in family called "OTIO_CORE", this is compiled into the + C++ core and represents the core interchange schemas of OpenTimelineIO. + + Users may define more family/label/schema:version mappings by way of the + version manifest plugins. + + Returns a dictionary mapping Schema name to schema version, like: + + .. code-block:: python + + { + "Clip": 2, + "Timeline": 1, + ... + } + + :param str family: family of labels (ie: "OTIO_CORE") + :param str label: label of schema-version map (ie: "0.15.0") + :returns: a dictionary mapping Schema name to schema version + :rtype: dict[str, int] + """ + + if family == "OTIO_CORE": + src = core.release_to_schema_version_map() + else: + src = plugins.ActiveManifest().version_manifests[family] + + return copy.deepcopy(src[label]) diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 02e7b39ba..00e545e6c 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -15,7 +15,7 @@ foreach(test ${tests_opentime}) WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}) endforeach() -list(APPEND tests_opentimelineio test_clip) +list(APPEND tests_opentimelineio test_clip test_serialization test_serializableCollection test_timeline test_track) foreach(test ${tests_opentimelineio}) add_executable(${test} utils.h utils.cpp ${test}.cpp) diff --git a/tests/__init__.py b/tests/__init__.py index e69de29bb..686a8cb5f 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -0,0 +1,2 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright Contributors to the OpenTimelineIO project diff --git a/tests/baseline_reader.py b/tests/baseline_reader.py index 63cc08612..4af7e2787 100755 --- a/tests/baseline_reader.py +++ b/tests/baseline_reader.py @@ -1,26 +1,5 @@ -# +# SPDX-License-Identifier: Apache-2.0 # Copyright Contributors to the OpenTimelineIO project -# -# Licensed under the Apache License, Version 2.0 (the "Apache License") -# with the following modification; you may not use this file except in -# compliance with the Apache License and the following modification to it: -# Section 6. Trademarks. is deleted and replaced with: -# -# 6. Trademarks. This License does not grant permission to use the trade -# names, trademarks, service marks, or product names of the Licensor -# and its affiliates, except as required to comply with Section 4(c) of -# the License and to reproduce the content of the NOTICE file. -# -# You may obtain a copy of the Apache License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the Apache License with the above modification is -# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -# KIND, either express or implied. See the Apache License for the specific -# language governing permissions and limitations under the Apache License. -# """Utilities for reading baseline json.""" @@ -55,7 +34,7 @@ def json_from_string(jsonstr): def json_from_file_as_string(fpath): - with open(fpath, 'r') as fo: + with open(fpath) as fo: return json_from_string(fo.read()) @@ -64,7 +43,7 @@ def path_to_baseline_directory(): def path_to_baseline(name): - return os.path.join(path_to_baseline_directory(), "{0}.json".format(name)) + return os.path.join(path_to_baseline_directory(), f"{name}.json") def json_baseline(name): diff --git a/tests/baselines/adapter_example.json b/tests/baselines/adapter_example.json index 58ff77f35..67d70b411 100644 --- a/tests/baselines/adapter_example.json +++ b/tests/baselines/adapter_example.json @@ -1,7 +1,6 @@ { "OTIO_SCHEMA" : "Adapter.1", "name" : "example", - "execution_scope" : "in process", "filepath" : "example.py", "suffixes" : ["example"] } diff --git a/tests/baselines/adapter_plugin_manifest.plugin_manifest.json b/tests/baselines/adapter_plugin_manifest.plugin_manifest.json index f3daafe0d..839dcadcc 100644 --- a/tests/baselines/adapter_plugin_manifest.plugin_manifest.json +++ b/tests/baselines/adapter_plugin_manifest.plugin_manifest.json @@ -23,5 +23,14 @@ "post_adapter_read" : [], "post_adapter_write" : ["post write example hook"], "post_media_linker" : ["example hook"] + }, + "version_manifests" : { + "TEST_FAMILY_NAME": { + "TEST_LABEL": { + "ExampleSchema":2, + "EnvVarTestSchema":1, + "Clip": 1 + } + } } } diff --git a/tests/baselines/example.py b/tests/baselines/example.py index 208ca0de5..b31abf983 100644 --- a/tests/baselines/example.py +++ b/tests/baselines/example.py @@ -1,26 +1,5 @@ -# +# SPDX-License-Identifier: Apache-2.0 # Copyright Contributors to the OpenTimelineIO project -# -# Licensed under the Apache License, Version 2.0 (the "Apache License") -# with the following modification; you may not use this file except in -# compliance with the Apache License and the following modification to it: -# Section 6. Trademarks. is deleted and replaced with: -# -# 6. Trademarks. This License does not grant permission to use the trade -# names, trademarks, service marks, or product names of the Licensor -# and its affiliates, except as required to comply with Section 4(c) of -# the License and to reproduce the content of the NOTICE file. -# -# You may obtain a copy of the Apache License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the Apache License with the above modification is -# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -# KIND, either express or implied. See the Apache License for the specific -# language governing permissions and limitations under the Apache License. -# """This file is here to support the test_adapter_plugin unittest. If you want to learn how to write your own adapter plugin, please read: diff --git a/tests/baselines/example_schemadef.py b/tests/baselines/example_schemadef.py index e96624f36..ae9b95907 100644 --- a/tests/baselines/example_schemadef.py +++ b/tests/baselines/example_schemadef.py @@ -1,26 +1,5 @@ -# +# SPDX-License-Identifier: Apache-2.0 # Copyright Contributors to the OpenTimelineIO project -# -# Licensed under the Apache License, Version 2.0 (the "Apache License") -# with the following modification; you may not use this file except in -# compliance with the Apache License and the following modification to it: -# Section 6. Trademarks. is deleted and replaced with: -# -# 6. Trademarks. This License does not grant permission to use the trade -# names, trademarks, service marks, or product names of the Licensor -# and its affiliates, except as required to comply with Section 4(c) of -# the License and to reproduce the content of the NOTICE file. -# -# You may obtain a copy of the Apache License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the Apache License with the above modification is -# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -# KIND, either express or implied. See the Apache License for the specific -# language governing permissions and limitations under the Apache License. -# """This file is here to support the test_schemadef_plugin unittest. If you want to learn how to write your own SchemaDef plugin, please read: diff --git a/tests/baselines/hookscript_example.json b/tests/baselines/hookscript_example.json index 7b9298d3d..af2bd0db9 100644 --- a/tests/baselines/hookscript_example.json +++ b/tests/baselines/hookscript_example.json @@ -1,6 +1,5 @@ { "OTIO_SCHEMA" : "HookScript.1", "name" : "example hook", - "execution_scope" : "in process", "filepath" : "example.py" } diff --git a/tests/baselines/media_linker_example.json b/tests/baselines/media_linker_example.json index 96945f66f..9a7c6663a 100644 --- a/tests/baselines/media_linker_example.json +++ b/tests/baselines/media_linker_example.json @@ -1,6 +1,5 @@ { "OTIO_SCHEMA" : "MediaLinker.1", "name" : "example", - "execution_scope" : "in process", "filepath" : "example.py" } diff --git a/tests/baselines/plugin_module/otio_jsonplugin/plugin_manifest.json b/tests/baselines/plugin_module/otio_jsonplugin/plugin_manifest.json index d526f968e..99e895d48 100644 --- a/tests/baselines/plugin_module/otio_jsonplugin/plugin_manifest.json +++ b/tests/baselines/plugin_module/otio_jsonplugin/plugin_manifest.json @@ -4,7 +4,6 @@ { "OTIO_SCHEMA": "Adapter.1", "name": "mock_adapter_json", - "execution_scope": "in process", "filepath": "adapter.py", "suffixes": ["mockadapter"] } @@ -13,7 +12,6 @@ { "OTIO_SCHEMA" : "MediaLinker.1", "name" : "mock_linker_json", - "execution_scope" : "in process", "filepath" : "linker.py" } ] diff --git a/tests/baselines/plugin_module/otio_mockplugin/__init__.py b/tests/baselines/plugin_module/otio_mockplugin/__init__.py index 52628e43a..67c6073c0 100644 --- a/tests/baselines/plugin_module/otio_mockplugin/__init__.py +++ b/tests/baselines/plugin_module/otio_mockplugin/__init__.py @@ -1,4 +1,8 @@ -import pkg_resources +# SPDX-License-Identifier: Apache-2.0 +# Copyright Contributors to the OpenTimelineIO project + +from importlib import resources +from pathlib import Path from opentimelineio.plugins import manifest @@ -17,13 +21,13 @@ def plugin_manifest(): - # XXX: in python3.5 resource_string returns a 'bytes' object, but - # json.loads requires a string, not bytes (only in 3.5 -- 2.7 and 3.6 - # seem to both be working). Luckily .decode() seems to work in both - # python3 and python2, so this *should* work for both versions. + try: + filepath = resources.files(__package__) / "unusually_named_plugin_manifest.json" + except AttributeError: + # For python <= 3.7 + with resources.path(__package__, "unusually_named_plugin_manifest.json") as p: + filepath = Path(p) + return manifest.manifest_from_string( - pkg_resources.resource_string( - __name__, - 'unusually_named_plugin_manifest.json' - ).decode('utf-8') + filepath.read_text() ) diff --git a/tests/baselines/plugin_module/otio_mockplugin/unusually_named_plugin_manifest.json b/tests/baselines/plugin_module/otio_mockplugin/unusually_named_plugin_manifest.json index da39edc55..3f3a59f44 100644 --- a/tests/baselines/plugin_module/otio_mockplugin/unusually_named_plugin_manifest.json +++ b/tests/baselines/plugin_module/otio_mockplugin/unusually_named_plugin_manifest.json @@ -4,7 +4,6 @@ { "OTIO_SCHEMA": "Adapter.1", "name": "mock_adapter", - "execution_scope": "in process", "filepath": "adapter.py", "suffixes": ["mockadapter"] } @@ -13,7 +12,6 @@ { "OTIO_SCHEMA" : "MediaLinker.1", "name" : "mock_linker", - "execution_scope" : "in process", "filepath" : "linker.py" } ] diff --git a/tests/baselines/plugin_module/otio_override_adapter.egg-info/PKG-INFO b/tests/baselines/plugin_module/otio_override_adapter.egg-info/PKG-INFO new file mode 100644 index 000000000..444aa45af --- /dev/null +++ b/tests/baselines/plugin_module/otio_override_adapter.egg-info/PKG-INFO @@ -0,0 +1,11 @@ +Metadata-Version: 1.0 +Name: otio-override-adapter +Version: 1.0.0 +Summary: Dummy Adapter used for testing. +Home-page: http://opentimeline.io +Author: Contributors to the OpenTimelineIO project +Author-email: otio-discussion@lists.aswf.io +License: Modified Apache 2.0 License +Description-Content-Type: UNKNOWN +Description: UNKNOWN +Platform: any diff --git a/tests/baselines/plugin_module/otio_override_adapter.egg-info/entry_points.txt b/tests/baselines/plugin_module/otio_override_adapter.egg-info/entry_points.txt new file mode 100644 index 000000000..d503f6174 --- /dev/null +++ b/tests/baselines/plugin_module/otio_override_adapter.egg-info/entry_points.txt @@ -0,0 +1,3 @@ +[opentimelineio.plugins] +mock_plugin = otio_override_adapter + diff --git a/contrib/opentimelineio_contrib/application_plugins/tests/__init__.py b/tests/baselines/plugin_module/otio_override_adapter/__init__.py similarity index 100% rename from contrib/opentimelineio_contrib/application_plugins/tests/__init__.py rename to tests/baselines/plugin_module/otio_override_adapter/__init__.py diff --git a/tests/baselines/plugin_module/otio_override_adapter/adapter.py b/tests/baselines/plugin_module/otio_override_adapter/adapter.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/baselines/plugin_module/otio_override_adapter/plugin_manifest.json b/tests/baselines/plugin_module/otio_override_adapter/plugin_manifest.json new file mode 100644 index 000000000..79f9a860d --- /dev/null +++ b/tests/baselines/plugin_module/otio_override_adapter/plugin_manifest.json @@ -0,0 +1,11 @@ +{ + "OTIO_SCHEMA" : "PluginManifest.1", + "adapters": [ + { + "OTIO_SCHEMA" : "Adapter.1", + "name" : "cmx_3600", + "filepath" : "adapter.py", + "suffixes" : ["edl"] + } + ] +} diff --git a/tests/baselines/post_write_example.py b/tests/baselines/post_write_example.py index f82eb9355..4c5e91bd2 100644 --- a/tests/baselines/post_write_example.py +++ b/tests/baselines/post_write_example.py @@ -1,26 +1,6 @@ -# +# SPDX-License-Identifier: Apache-2.0 # Copyright Contributors to the OpenTimelineIO project -# -# Licensed under the Apache License, Version 2.0 (the "Apache License") -# with the following modification; you may not use this file except in -# compliance with the Apache License and the following modification to it: -# Section 6. Trademarks. is deleted and replaced with: -# -# 6. Trademarks. This License does not grant permission to use the trade -# names, trademarks, service marks, or product names of the Licensor -# and its affiliates, except as required to comply with Section 4(c) of -# the License and to reproduce the content of the NOTICE file. -# -# You may obtain a copy of the Apache License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the Apache License with the above modification is -# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -# KIND, either express or implied. See the Apache License for the specific -# language governing permissions and limitations under the Apache License. -# + import os """This file is here to support the test_adapter_plugin unittest. diff --git a/tests/baselines/post_write_hookscript_example.json b/tests/baselines/post_write_hookscript_example.json index 6a820ded1..e58f63153 100644 --- a/tests/baselines/post_write_hookscript_example.json +++ b/tests/baselines/post_write_hookscript_example.json @@ -1,6 +1,5 @@ { "OTIO_SCHEMA" : "HookScript.1", "name" : "post write example hook", - "execution_scope" : "in process", "filepath" : "post_write_example.py" } diff --git a/tests/baselines/schemadef_example.json b/tests/baselines/schemadef_example.json index 2f178bbba..7ec61c4aa 100644 --- a/tests/baselines/schemadef_example.json +++ b/tests/baselines/schemadef_example.json @@ -4,7 +4,6 @@ { "OTIO_SCHEMA" : "SchemaDef.1", "name" : "example_schemadef", - "execution_scope" : "in process", "filepath" : "example_schemadef.py" } ] diff --git a/tests/consumer/CMakeLists.txt b/tests/consumer/CMakeLists.txt new file mode 100644 index 000000000..1a851f9e3 --- /dev/null +++ b/tests/consumer/CMakeLists.txt @@ -0,0 +1,6 @@ +cmake_minimum_required(VERSION 3.18.2 FATAL_ERROR) + +project(consumer_tests) + +add_subdirectory(opentime) +add_subdirectory(opentimeline) diff --git a/tests/consumer/opentime/CMakeLists.txt b/tests/consumer/opentime/CMakeLists.txt new file mode 100644 index 000000000..d9a682930 --- /dev/null +++ b/tests/consumer/opentime/CMakeLists.txt @@ -0,0 +1,2 @@ +find_package(OpenTime REQUIRED) +message(STATUS "Found OpenTime successfully at '${OpenTime_DIR}'") diff --git a/tests/consumer/opentimeline/CMakeLists.txt b/tests/consumer/opentimeline/CMakeLists.txt new file mode 100644 index 000000000..660a20be7 --- /dev/null +++ b/tests/consumer/opentimeline/CMakeLists.txt @@ -0,0 +1,2 @@ +find_package(OpenTimelineIO REQUIRED) +message(STATUS "Found OpenTimelineIO successfully at '${OpenTimelineIO_DIR}'") diff --git a/tests/sample_data/avid_example.edl b/tests/sample_data/avid_example.edl new file mode 100644 index 000000000..243ba5e91 --- /dev/null +++ b/tests/sample_data/avid_example.edl @@ -0,0 +1,7 @@ +TITLE: Avid_Example.01 +001 ZZ100_50 V C 01:00:04:05 01:00:05:12 00:59:53:11 00:59:54:18 +* FROM CLIP NAME: take_1 +* FROM CLIP: S:\path\to\ZZ100_501.take_1.0001.exr +002 ZZ100_50 V C 01:00:06:13 01:00:08:15 00:59:54:18 00:59:56:20 +* FROM CLIP NAME: take_2 +* FROM CLIP: S:\path\to\ZZ100_502A.take_2.0101.exr diff --git a/tests/sample_data/dissolve_test_3.edl b/tests/sample_data/dissolve_test_3.edl index 46f6d8a74..8d875147d 100644 --- a/tests/sample_data/dissolve_test_3.edl +++ b/tests/sample_data/dissolve_test_3.edl @@ -2,14 +2,14 @@ TITLE: dissolve test 3 FCM: NON-DROP FRAME 001 AX V C 01:00:03:23 01:00:06:12 01:00:00:00 01:00:02:13 -* FROM CLIP NAME: Clip A.mov +* FROM CLIP NAME: Clip_A.mov 002 AX V C 01:00:06:00 01:00:06:00 01:00:02:13 01:00:02:13 FCM: NON-DROP FRAME 002 AX V D 030 01:00:33:22 01:00:35:04 01:00:02:13 01:00:03:19 EFFECTS NAME IS CROSS DISSOLVE -* FROM CLIP NAME: Clip B.mov -* TO CLIP NAME: Clip C.mov +* FROM CLIP NAME: Clip_B.mov +* TO CLIP NAME: Clip_C.mov 003 AX V C 01:00:00:00 01:00:01:22 01:00:03:19 01:00:05:17 -* FROM CLIP NAME: Clip D.mov +* FROM CLIP NAME: Clip_D.mov diff --git a/tests/sample_data/dissolve_test_4.edl b/tests/sample_data/dissolve_test_4.edl index 7d59ceb1c..1263d8339 100644 --- a/tests/sample_data/dissolve_test_4.edl +++ b/tests/sample_data/dissolve_test_4.edl @@ -2,10 +2,10 @@ TITLE: TRANSITION_TEST_2 FCM: NON-DROP FRAME 001 ABC0000. V C 01:00:06:18 01:00:08:00 01:04:11:17 01:04:12:23 002 ABC0010. V C 01:00:06:15 01:00:08:18 01:04:12:23 01:04:15:02 -003 ABC0010. V C 01:00:08:18 01:00:08:18 01:04:15:02 01:04:15:02 +003 ABC0020. V C 01:00:08:18 01:00:08:18 01:04:15:02 01:04:15:02 003 ABC0020. V D 035 01:00:06:22 01:00:10:07 01:04:15:02 01:04:18:11 * BLEND, DISSOLVE -004 ABC0020. V C 01:00:10:07 01:00:10:07 01:04:18:11 01:04:18:11 +004 ABC0030. V C 01:00:10:07 01:00:10:07 01:04:18:11 01:04:18:11 004 ABC0030. V D 064 01:00:06:10 01:00:09:22 01:04:18:11 01:04:21:23 * BLEND, DISSOLVE 005 ABC0040. V C 01:00:08:14 01:00:12:14 01:04:21:23 01:04:25:23 diff --git a/tests/sample_data/premiere_example.edl b/tests/sample_data/premiere_example.edl new file mode 100644 index 000000000..0ff55c0a9 --- /dev/null +++ b/tests/sample_data/premiere_example.edl @@ -0,0 +1,5 @@ +TITLE: Premiere_Example.01 +001 AX V C 01:00:04:05 01:00:05:12 00:59:53:11 00:59:54:18 +* FROM CLIP NAME: ZZ100_501.take_1.0001.exr +002 AX V C 01:00:06:13 01:00:08:15 00:59:54:18 00:59:56:20 +* FROM CLIP NAME: ZZ100_502A.take_2.0101.exr diff --git a/tests/test_adapter_plugin.py b/tests/test_adapter_plugin.py index b7e92c685..cd14f6392 100755 --- a/tests/test_adapter_plugin.py +++ b/tests/test_adapter_plugin.py @@ -1,26 +1,6 @@ -# +# SPDX-License-Identifier: Apache-2.0 # Copyright Contributors to the OpenTimelineIO project -# -# Licensed under the Apache License, Version 2.0 (the "Apache License") -# with the following modification; you may not use this file except in -# compliance with the Apache License and the following modification to it: -# Section 6. Trademarks. is deleted and replaced with: -# -# 6. Trademarks. This License does not grant permission to use the trade -# names, trademarks, service marks, or product names of the Licensor -# and its affiliates, except as required to comply with Section 4(c) of -# the License and to reproduce the content of the NOTICE file. -# -# You may obtain a copy of the Apache License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the Apache License with the above modification is -# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -# KIND, either express or implied. See the Apache License for the specific -# language governing permissions and limitations under the Apache License. -# + import unittest import os @@ -28,14 +8,7 @@ from tests import baseline_reader, utils -# handle python2 vs python3 difference -try: - from tempfile import TemporaryDirectory # noqa: F401 - import tempfile -except ImportError: - # XXX: python2.7 only - from backports import tempfile - +import tempfile """Unit tests for the adapter plugin system.""" @@ -50,7 +23,7 @@ def test_supported_suffixes_is_not_none(self): result = otio.adapters.suffixes_with_defined_adapters() self.assertIsNotNone(result) self.assertNotEqual(result, []) - self.assertNotEqual(result, set([])) + self.assertNotEqual(result, set()) class TestPluginAdapters(unittest.TestCase): @@ -65,21 +38,18 @@ def setUp(self): def test_plugin_adapter(self): self.assertEqual(self.adp.name, "example") - self.assertEqual(self.adp.execution_scope, "in process") self.assertEqual(self.adp.filepath, "example.py") - self.assertEqual(self.adp.suffixes[0], u"example") - self.assertEqual(list(self.adp.suffixes), [u'example']) + self.assertEqual(self.adp.suffixes[0], "example") + self.assertEqual(list(self.adp.suffixes), ['example']) self.assertMultiLineEqual( str(self.adp), "Adapter(" "{}, " "{}, " - "{}, " "{}" ")".format( repr(self.adp.name), - repr(self.adp.execution_scope), repr(self.adp.filepath), repr(self.adp.suffixes), ) @@ -88,12 +58,10 @@ def test_plugin_adapter(self): repr(self.adp), "otio.adapter.Adapter(" "name={}, " - "execution_scope={}, " "filepath={}, " "suffixes={}" ")".format( repr(self.adp.name), - repr(self.adp.execution_scope), repr(self.adp.filepath), repr(self.adp.suffixes), ) @@ -242,7 +210,8 @@ def test_deduplicate_env_variable_paths(self): if bak_env is not None: os.environ['OTIO_PLUGIN_MANIFEST_PATH'] = bak_env else: - del os.environ['OTIO_PLUGIN_MANIFEST_PATH'] + if "OTIO_PLUGIN_MANIFEST_PATH" in os.environ: + del os.environ['OTIO_PLUGIN_MANIFEST_PATH'] def test_find_manifest_by_environment_variable(self): basename = "unittest.plugin_manifest.json" @@ -320,7 +289,6 @@ def test_plugin_manifest_order(self): { "OTIO_SCHEMA": "Adapter.1", "name": "local_json", - "execution_scope": "in process", "filepath": "example.py", "suffixes": ["example"] } diff --git a/tests/test_box2d.py b/tests/test_box2d.py index 76665b6bf..93b7cad39 100644 --- a/tests/test_box2d.py +++ b/tests/test_box2d.py @@ -1,26 +1,6 @@ -# +# SPDX-License-Identifier: Apache-2.0 # Copyright Contributors to the OpenTimelineIO project -# -# Licensed under the Apache License, Version 2.0 (the "Apache License") -# with the following modification; you may not use this file except in -# compliance with the Apache License and the following modification to it: -# Section 6. Trademarks. is deleted and replaced with: -# -# 6. Trademarks. This License does not grant permission to use the trade -# names, trademarks, service marks, or product names of the Licensor -# and its affiliates, except as required to comply with Section 4(c) of -# the License and to reproduce the content of the NOTICE file. -# -# You may obtain a copy of the Apache License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the Apache License with the above modification is -# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -# KIND, either express or implied. See the Apache License for the specific -# language governing permissions and limitations under the Apache License. -# + import unittest import opentimelineio as otio diff --git a/tests/test_builtin_adapters.py b/tests/test_builtin_adapters.py index 8eb5a7790..c3f171c6d 100755 --- a/tests/test_builtin_adapters.py +++ b/tests/test_builtin_adapters.py @@ -1,26 +1,5 @@ -# +# SPDX-License-Identifier: Apache-2.0 # Copyright Contributors to the OpenTimelineIO project -# -# Licensed under the Apache License, Version 2.0 (the "Apache License") -# with the following modification; you may not use this file except in -# compliance with the Apache License and the following modification to it: -# Section 6. Trademarks. is deleted and replaced with: -# -# 6. Trademarks. This License does not grant permission to use the trade -# names, trademarks, service marks, or product names of the Licensor -# and its affiliates, except as required to comply with Section 4(c) of -# the License and to reproduce the content of the NOTICE file. -# -# You may obtain a copy of the Apache License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the Apache License with the above modification is -# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -# KIND, either express or implied. See the Apache License for the specific -# language governing permissions and limitations under the Apache License. -# """Test builtin adapters.""" @@ -34,13 +13,7 @@ otio_json, ) -# handle python2 vs python3 difference -try: - from tempfile import TemporaryDirectory # noqa: F401 - import tempfile -except ImportError: - # XXX: python2.7 only - from backports import tempfile +import tempfile SAMPLE_DATA_DIR = os.path.join(os.path.dirname(__file__), "sample_data") @@ -86,9 +59,18 @@ def test_disk_vs_string(self): temp_file = os.path.join(temp_dir, "test_disk_vs_string.otio") otio.adapters.write_to_file(timeline, temp_file) in_memory = otio.adapters.write_to_string(timeline, 'otio_json') - with open(temp_file, 'r') as f: + with open(temp_file) as f: on_disk = f.read() + self.maxDiff = None + + # for debugging + # with open("/var/tmp/in_memory.otio", "w") as fo: + # fo.write(in_memory) + # + # with open("/var/tmp/on_disk.otio", "w") as fo: + # fo.write(on_disk) + self.assertEqual(in_memory, on_disk) def test_adapters_fetch(self): diff --git a/tests/test_cdl.py b/tests/test_cdl.py index 32f08be8f..bbb8694c6 100755 --- a/tests/test_cdl.py +++ b/tests/test_cdl.py @@ -1,26 +1,5 @@ -# +# SPDX-License-Identifier: Apache-2.0 # Copyright Contributors to the OpenTimelineIO project -# -# Licensed under the Apache License, Version 2.0 (the "Apache License") -# with the following modification; you may not use this file except in -# compliance with the Apache License and the following modification to it: -# Section 6. Trademarks. is deleted and replaced with: -# -# 6. Trademarks. This License does not grant permission to use the trade -# names, trademarks, service marks, or product names of the Licensor -# and its affiliates, except as required to comply with Section 4(c) of -# the License and to reproduce the content of the NOTICE file. -# -# You may obtain a copy of the Apache License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the Apache License with the above modification is -# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -# KIND, either express or implied. See the Apache License for the specific -# language governing permissions and limitations under the Apache License. -# # python import os @@ -110,7 +89,7 @@ def test_cdl_read_with_commas(self): ) for value_comp, ref_comp in comparisons: self.assertAlmostEqual( - value_comp, ref_comp, msg="mismatch in {}".format(function) + value_comp, ref_comp, msg=f"mismatch in {function}" ) def test_cdl_round_trip(self): diff --git a/tests/test_clip.cpp b/tests/test_clip.cpp index 6253e8c42..8dac199bc 100644 --- a/tests/test_clip.cpp +++ b/tests/test_clip.cpp @@ -1,9 +1,13 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Contributors to the OpenTimelineIO project + #include "utils.h" #include #include #include #include +#include #include #include diff --git a/tests/test_clip.py b/tests/test_clip.py index dea5aad65..1cab19b18 100644 --- a/tests/test_clip.py +++ b/tests/test_clip.py @@ -1,26 +1,5 @@ -# +# SPDX-License-Identifier: Apache-2.0 # Copyright Contributors to the OpenTimelineIO project -# -# Licensed under the Apache License, Version 2.0 (the "Apache License") -# with the following modification; you may not use this file except in -# compliance with the Apache License and the following modification to it: -# Section 6. Trademarks. is deleted and replaced with: -# -# 6. Trademarks. This License does not grant permission to use the trade -# names, trademarks, service marks, or product names of the Licensor -# and its affiliates, except as required to comply with Section 4(c) of -# the License and to reproduce the content of the NOTICE file. -# -# You may obtain a copy of the Apache License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the Apache License with the above modification is -# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -# KIND, either express or implied. See the Apache License for the specific -# language governing permissions and limitations under the Apache License. -# import unittest @@ -57,9 +36,9 @@ def test_cons(self): decoded = otio.adapters.otio_json.read_from_string(encoded) self.assertIsOTIOEquivalentTo(cl, decoded) - def test_each_clip(self): + def test_find_clips(self): cl = otio.schema.Clip(name="test_clip") - self.assertEqual(list(cl.each_clip()), [cl]) + self.assertEqual(list(cl.find_clips()), [cl]) def test_str(self): cl = otio.schema.Clip(name="test_clip") @@ -166,6 +145,11 @@ def test_available_image_bounds(self): self.assertEqual(16.0, cl.available_image_bounds.max.x) self.assertEqual(9.0, cl.available_image_bounds.max.y) + # test range exceptions + cl.media_reference.available_image_bounds = None + with self.assertRaises(otio.exceptions.CannotComputeAvailableRangeError): + cl.available_range() + def test_ref_default(self): cl = otio.schema.Clip() self.assertIsOTIOEquivalentTo( diff --git a/tests/test_cmx_3600_adapter.py b/tests/test_cmx_3600_adapter.py index 83c783c94..b07346865 100755 --- a/tests/test_cmx_3600_adapter.py +++ b/tests/test_cmx_3600_adapter.py @@ -1,27 +1,7 @@ #!/usr/bin/env python # +# SPDX-License-Identifier: Apache-2.0 # Copyright Contributors to the OpenTimelineIO project -# -# Licensed under the Apache License, Version 2.0 (the "Apache License") -# with the following modification; you may not use this file except in -# compliance with the Apache License and the following modification to it: -# Section 6. Trademarks. is deleted and replaced with: -# -# 6. Trademarks. This License does not grant permission to use the trade -# names, trademarks, service marks, or product names of the Licensor -# and its affiliates, except as required to comply with Section 4(c) of -# the License and to reproduce the content of the NOTICE file. -# -# You may obtain a copy of the Apache License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the Apache License with the above modification is -# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -# KIND, either express or implied. See the Apache License for the specific -# language governing permissions and limitations under the Apache License. -# """Test the CMX 3600 EDL adapter.""" @@ -33,18 +13,14 @@ import opentimelineio.test_utils as otio_test_utils from opentimelineio.adapters import cmx_3600 -# handle python2 vs python3 difference -try: - from tempfile import TemporaryDirectory # noqa: F401 - import tempfile -except ImportError: - # XXX: python2.7 only - from backports import tempfile - +from tempfile import TemporaryDirectory # noqa: F401 +import tempfile SAMPLE_DATA_DIR = os.path.join(os.path.dirname(__file__), "sample_data") SCREENING_EXAMPLE_PATH = os.path.join(SAMPLE_DATA_DIR, "screening_example.edl") +AVID_EXAMPLE_PATH = os.path.join(SAMPLE_DATA_DIR, "avid_example.edl") NUCODA_EXAMPLE_PATH = os.path.join(SAMPLE_DATA_DIR, "nucoda_example.edl") +PREMIERE_EXAMPLE_PATH = os.path.join(SAMPLE_DATA_DIR, "premiere_example.edl") EXEMPLE_25_FPS_PATH = os.path.join(SAMPLE_DATA_DIR, "25fps.edl") NO_SPACES_PATH = os.path.join(SAMPLE_DATA_DIR, "no_spaces_test.edl") DISSOLVE_TEST = os.path.join(SAMPLE_DATA_DIR, "dissolve_test.edl") @@ -55,10 +31,7 @@ WIPE_TEST = os.path.join(SAMPLE_DATA_DIR, "wipe_test.edl") TIMECODE_MISMATCH_TEST = os.path.join(SAMPLE_DATA_DIR, "timecode_mismatch.edl") SPEED_EFFECTS_TEST = os.path.join(SAMPLE_DATA_DIR, "speed_effects.edl") -SPEED_EFFECTS_TEST_SMALL = os.path.join( - SAMPLE_DATA_DIR, - "speed_effects_small.edl" -) +SPEED_EFFECTS_TEST_SMALL = os.path.join(SAMPLE_DATA_DIR, "speed_effects_small.edl") MULTIPLE_TARGET_AUDIO_PATH = os.path.join(SAMPLE_DATA_DIR, "multi_audio.edl") TRANSITION_DURATION_TEST = os.path.join(SAMPLE_DATA_DIR, "transition_duration.edl") ENABLED_TEST = os.path.join(SAMPLE_DATA_DIR, "enabled.otio") @@ -371,8 +344,8 @@ def test_edl_round_trip_disk2mem2disk(self): # os.system("opendiff {} {}".format(SCREENING_EXAMPLE_PATH, tmp_path)) # But the EDL text on disk are *not* byte-for-byte identical - with open(SCREENING_EXAMPLE_PATH, "r") as original_file: - with open(tmp_path, "r") as output_file: + with open(SCREENING_EXAMPLE_PATH) as original_file: + with open(tmp_path) as output_file: self.assertNotEqual(original_file.read(), output_file.read()) def test_regex_flexibility(self): @@ -510,51 +483,76 @@ def test_imagesequence_write(self): def test_dissolve_parse(self): tl = otio.adapters.read_from_file(DISSOLVE_TEST) - self.assertEqual(len(tl.tracks[0]), 3) + # clip/transition/clip/clip + self.assertEqual(len(tl.tracks[0]), 4) self.assertTrue(isinstance(tl.tracks[0][1], otio.schema.Transition)) - - self.assertEqual(tl.tracks[0][0].duration().value, 14) - self.assertEqual(tl.tracks[0][2].duration().value, 6) + self.assertEqual(tl.tracks[0][0].duration().value, 9) + # The visible range must contains all the frames needed for the transition + # Edit duration + transition duration + self.assertEqual(tl.tracks[0][0].visible_range().duration.to_frames(), 19) + self.assertEqual(tl.tracks[0][0].name, "clip_A") + self.assertEqual(tl.tracks[0][1].duration().value, 10) + self.assertEqual(tl.tracks[0][1].name, "SMPTE_Dissolve from clip_A to clip_B") + self.assertEqual(tl.tracks[0][2].duration().value, 10) + self.assertEqual(tl.tracks[0][2].visible_range().duration.value, 10) + self.assertEqual(tl.tracks[0][2].name, "clip_B") + self.assertEqual(tl.tracks[0][3].duration().value, 1) + self.assertEqual(tl.tracks[0][2].name, "clip_B") def test_dissolve_parse_middle(self): tl = otio.adapters.read_from_file(DISSOLVE_TEST_2) - self.assertEqual(len(tl.tracks[0]), 3) + trck = tl.tracks[0] + # 3 clips and 1 transition + self.assertEqual(len(trck), 4) - self.assertTrue(isinstance(tl.tracks[0][1], otio.schema.Transition)) + self.assertTrue(isinstance(trck[1], otio.schema.Transition)) - trck = tl.tracks[0] - self.assertEqual(trck[0].duration().value, 10) - self.assertEqual(trck[2].source_range.start_time.value, 86400 + 201) + self.assertEqual(trck[0].duration().value, 5) + self.assertEqual(trck[0].visible_range().duration.to_frames(), 15) + self.assertEqual(trck[1].duration().value, 10) + self.assertEqual(trck[1].name, "SMPTE_Dissolve from clip_A to clip_B") + + self.assertEqual( + trck[2].source_range.start_time.value, + otio.opentime.from_timecode('01:00:08:04', 24).value + ) + self.assertEqual(trck[2].name, "clip_B") self.assertEqual(trck[2].duration().value, 10) + self.assertEqual(trck[2].visible_range().duration.value, 10) + + self.assertEqual(tl.tracks[0][0].visible_range().duration.to_frames(), 15) def test_dissolve_parse_full_clip_dissolve(self): tl = otio.adapters.read_from_file(DISSOLVE_TEST_3) - self.assertEqual(len(tl.tracks[0]), 5) + self.assertEqual(len(tl.tracks[0]), 4) - self.assertTrue(isinstance(tl.tracks[0][2], otio.schema.Transition)) + self.assertTrue(isinstance(tl.tracks[0][1], otio.schema.Transition)) trck = tl.tracks[0] clip_a = trck[0] - self.assertEqual(clip_a.name, "Clip A.mov") + self.assertEqual(clip_a.name, "Clip_A.mov") self.assertEqual(clip_a.duration().value, 61) + self.assertEqual(clip_a.visible_range().duration.value, 61 + 30) - clip_b = trck[1] - self.assertEqual(clip_b.name, "Clip B.mov") - self.assertEqual(clip_b.source_range.start_time.value, 86400 + 144) - self.assertEqual(clip_b.duration().value, 15) - - transition = trck[2] - self.assertEqual(transition.in_offset.value, 15) - self.assertEqual(transition.out_offset.value, 15) + transition = trck[1] + # Note: clip names in the EDL are wrong, the transition is actually + # from Clip_A to Clip_B + self.assertEqual( + transition.name, + "SMPTE_Dissolve from Clip_B.mov to Clip_C.mov" + ) + self.assertEqual(transition.in_offset.value, 0) + self.assertEqual(transition.out_offset.value, 30) - clip_c = trck[3] - self.assertEqual(clip_c.name, "Clip C.mov") - self.assertEqual(clip_c.source_range.start_time.value, 86400 + 829) - self.assertEqual(clip_c.duration().value, 15) + clip_c = trck[2] + self.assertEqual(clip_c.name, "Clip_C.mov") + self.assertEqual(clip_c.source_range.start_time.value, 86400 + (33 * 24 + 22)) + self.assertEqual(clip_c.duration().value, 30) + self.assertEqual(clip_c.visible_range().duration.value, 30) - clip_d = trck[4] - self.assertEqual(clip_d.name, "Clip D.mov") + clip_d = trck[3] + self.assertEqual(clip_d.name, "Clip_D.mov") self.assertEqual(clip_d.source_range.start_time.value, 86400) self.assertEqual(clip_d.duration().value, 46) @@ -573,18 +571,22 @@ def test_dissolve_with_odd_frame_count_maintains_length(self): def test_wipe_parse(self): tl = otio.adapters.read_from_file(WIPE_TEST) - self.assertEqual(len(tl.tracks[0]), 3) + self.assertEqual(len(tl.tracks[0]), 4) wipe = tl.tracks[0][1] self.assertTrue(isinstance(wipe, otio.schema.Transition)) - self.assertEqual(wipe.transition_type, "SMPTE_Wipe") self.assertEqual(wipe.metadata["cmx_3600"]["transition"], "W001") - self.assertEqual(tl.tracks[0][0].duration().value, 14) - self.assertEqual(tl.tracks[0][2].duration().value, 6) + self.assertEqual(tl.tracks[0][0].duration().value, 9) + self.assertEqual(tl.tracks[0][0].visible_range().duration.value, 19) - def test_fade_to_black_ends_with_gap(self): + self.assertEqual(tl.tracks[0][2].duration().value, 10) + self.assertEqual(tl.tracks[0][2].visible_range().duration.value, 10) + + self.assertEqual(tl.tracks[0][3].duration().value, 1) + + def test_fade_to_black(self): # EXERCISE tl = otio.adapters.read_from_string( '1 CLPA V C 00:00:03:18 00:00:12:15 00:00:00:00 00:00:08:21\n' @@ -596,10 +598,45 @@ def test_fade_to_black_ends_with_gap(self): # VALIDATE self.assertEqual(len(tl.tracks[0]), 3) self.assertTrue(isinstance(tl.tracks[0][1], otio.schema.Transition)) - self.assertTrue(isinstance(tl.tracks[0][2], otio.schema.Gap)) - self.assertEqual(tl.tracks[0][2].duration().value, 12) + self.assertTrue(isinstance(tl.tracks[0][2], otio.schema.Clip)) + self.assertEqual(tl.tracks[0][2].media_reference.generator_kind, 'black') + self.assertEqual(tl.tracks[0][2].duration().value, 24) self.assertEqual(tl.tracks[0][2].source_range.start_time.value, 0) + def test_edl_round_trip_with_transitions(self): + with tempfile.TemporaryDirectory() as temp_dir: + # Notes: + # - the writer does not handle wipes, only dissolves + # - the writer can generate invalid EDLs if spaces are in reel names. + for edl_file in [ + DISSOLVE_TEST, + DISSOLVE_TEST_2, + DISSOLVE_TEST_3, + DISSOLVE_TEST_4 + ]: + edl_name = os.path.basename(edl_file) + timeline = otio.adapters.read_from_file(edl_file) + tmp_path = os.path.join( + temp_dir, + f'test_edl_round_trip_{edl_name}' + ) + otio.adapters.write_to_file(timeline, tmp_path) + + result = otio.adapters.read_from_file(tmp_path) + self.assertEqual(len(timeline.tracks), len(result.tracks)) + for track, res_track in zip(timeline.tracks, result.tracks): + self.assertEqual(len(track), len(res_track)) + for child, res_child in zip(track, res_track): + self.assertEqual(type(child), type(res_child)) + if isinstance(child, otio.schema.Transition): + self.assertEqual(child.in_offset, res_child.in_offset) + self.assertEqual(child.out_offset, res_child.out_offset) + self.assertEqual( + child.transition_type, res_child.transition_type + ) + else: + self.assertEqual(child.source_range, res_child.source_range) + def test_edl_25fps(self): # EXERCISE edl_path = EXEMPLE_25_FPS_PATH @@ -656,8 +693,8 @@ def test_read_generators(self): # EXERCISE tl = otio.adapters.read_from_string( '1 BL V C 00:00:00:00 00:00:01:00 00:00:00:00 00:00:01:00\n' - '1 BLACK V C 00:00:00:00 00:00:01:00 00:00:01:00 00:00:02:00\n' - '1 BARS V C 00:00:00:00 00:00:01:00 00:00:02:00 00:00:03:00\n', + '2 BLACK V C 00:00:00:00 00:00:01:00 00:00:01:00 00:00:02:00\n' + '3 BARS V C 00:00:00:00 00:00:01:00 00:00:02:00 00:00:03:00\n', adapter_name="cmx_3600" ) @@ -675,47 +712,81 @@ def test_read_generators(self): 'SMPTEBars' ) - def test_nucoda_edl_read(self): - edl_path = NUCODA_EXAMPLE_PATH - fps = 24 - timeline = otio.adapters.read_from_file(edl_path) - self.assertTrue(timeline is not None) - self.assertEqual(len(timeline.tracks), 1) - self.assertEqual(len(timeline.tracks[0]), 2) - self.assertEqual( - timeline.tracks[0][0].name, - "take_1" - ) - self.assertEqual( - timeline.tracks[0][0].source_range.duration, - otio.opentime.from_timecode("00:00:01:07", fps) - ) - self.assertIsOTIOEquivalentTo( - timeline.tracks[0][0].media_reference, - otio.schema.ExternalReference( - target_url=r"S:\path\to\ZZ100_501.take_1.0001.exr" + def test_style_edl_read(self): + edl_paths = [AVID_EXAMPLE_PATH, NUCODA_EXAMPLE_PATH, PREMIERE_EXAMPLE_PATH] + for edl_path in edl_paths: + fps = 24 + timeline = otio.adapters.read_from_file(edl_path) + self.assertTrue(timeline is not None) + self.assertEqual(len(timeline.tracks), 1) + self.assertEqual(len(timeline.tracks[0]), 2) + print(edl_path) + + # If cannot assertEqual fails with clip name + # Attempt to assertEqual with + try: + self.assertEqual( + timeline.tracks[0][0].name, + "take_1" + ) + except AssertionError: + self.assertEqual( + timeline.tracks[0][0].name, + "ZZ100_501.take_1.0001.exr" + ) + self.assertEqual( + timeline.tracks[0][0].source_range.duration, + otio.opentime.from_timecode("00:00:01:07", fps) ) - ) - self.assertEqual( - timeline.tracks[0][1].name, - "take_2" - ) - self.assertEqual( - timeline.tracks[0][1].source_range.duration, - otio.opentime.from_timecode("00:00:02:02", fps) - ) - self.assertIsOTIOEquivalentTo( - timeline.tracks[0][1].media_reference, - otio.schema.ExternalReference( - target_url=r"S:\path\to\ZZ100_502A.take_2.0101.exr" + print(timeline.tracks[0][0].media_reference) + + try: + self.assertIsOTIOEquivalentTo( + timeline.tracks[0][0].media_reference, + otio.schema.ExternalReference( + target_url=r"S:\path\to\ZZ100_501.take_1.0001.exr" + ) + ) + except AssertionError: + self.assertIsOTIOEquivalentTo( + timeline.tracks[0][0].media_reference, + otio.schema.MissingReference() + ) + + try: + self.assertEqual( + timeline.tracks[0][1].name, + "take_2" + ) + except AssertionError: + self.assertEqual( + timeline.tracks[0][1].name, + "ZZ100_502A.take_2.0101.exr" + ) + + self.assertEqual( + timeline.tracks[0][1].source_range.duration, + otio.opentime.from_timecode("00:00:02:02", fps) ) - ) - def test_nucoda_edl_write(self): + try: + self.assertIsOTIOEquivalentTo( + timeline.tracks[0][1].media_reference, + otio.schema.ExternalReference( + target_url=r"S:\path\to\ZZ100_502A.take_2.0101.exr" + ) + ) + except AssertionError: + self.assertIsOTIOEquivalentTo( + timeline.tracks[0][1].media_reference, + otio.schema.MissingReference() + ) + + def test_style_edl_write(self): track = otio.schema.Track() - tl = otio.schema.Timeline("test_nucoda_timeline", tracks=[track]) + tl = otio.schema.Timeline("temp", tracks=[track]) rt = otio.opentime.RationalTime(5.0, 24.0) - mr = otio.schema.ExternalReference(target_url=r"S:\var\tmp\test.exr") + mr = otio.schema.ExternalReference(target_url=r"S:/var/tmp/test.exr") tr = otio.opentime.TimeRange( start_time=otio.opentime.RationalTime(0.0, 24.0), @@ -742,6 +813,7 @@ def test_nucoda_edl_write(self): tl.tracks[0].append(gap) tl.tracks[0].append(cl2) + tl.name = 'test_nucoda_timeline' result = otio.adapters.write_to_string( tl, adapter_name='cmx_3600', @@ -752,11 +824,53 @@ def test_nucoda_edl_write(self): 001 test V C 00:00:00:00 00:00:00:05 00:00:00:00 00:00:00:05 * FROM CLIP NAME: test clip1 -* FROM FILE: S:\var\tmp\test.exr +* FROM FILE: S:/var/tmp/test.exr * OTIO TRUNCATED REEL NAME FROM: test.exr 002 test V C 00:00:00:00 00:00:00:05 00:00:01:05 00:00:01:10 * FROM CLIP NAME: test clip2 -* FROM FILE: S:\var\tmp\test.exr +* FROM FILE: S:/var/tmp/test.exr +* OTIO TRUNCATED REEL NAME FROM: test.exr +''' + + self.assertMultiLineEqual(result, expected) + + tl.name = 'test_avid_timeline' + result = otio.adapters.write_to_string( + tl, + adapter_name='cmx_3600', + style='avid' + ) + + expected = r'''TITLE: test_avid_timeline + +001 test V C 00:00:00:00 00:00:00:05 00:00:00:00 00:00:00:05 +* FROM CLIP NAME: test clip1 +* FROM CLIP: S:/var/tmp/test.exr +* OTIO TRUNCATED REEL NAME FROM: test.exr +002 test V C 00:00:00:00 00:00:00:05 00:00:01:05 00:00:01:10 +* FROM CLIP NAME: test clip2 +* FROM CLIP: S:/var/tmp/test.exr +* OTIO TRUNCATED REEL NAME FROM: test.exr +''' + + self.assertMultiLineEqual(result, expected) + + tl.name = 'test_premiere_timeline' + result = otio.adapters.write_to_string( + tl, + adapter_name='cmx_3600', + style='premiere' + ) + + expected = r'''TITLE: test_premiere_timeline + +001 AX V C 00:00:00:00 00:00:00:05 00:00:00:00 00:00:00:05 +* FROM CLIP NAME: test.exr +* OTIO REFERENCE FROM: S:/var/tmp/test.exr +* OTIO TRUNCATED REEL NAME FROM: test.exr +002 AX V C 00:00:00:00 00:00:00:05 00:00:01:05 00:00:01:10 +* FROM CLIP NAME: test.exr +* OTIO REFERENCE FROM: S:/var/tmp/test.exr * OTIO TRUNCATED REEL NAME FROM: test.exr ''' @@ -768,10 +882,10 @@ def test_reels_edl_round_trip_string2mem2string(self): 001 ZZ100_50 V C 01:00:04:05 01:00:05:12 00:59:53:11 00:59:54:18 * FROM CLIP NAME: take_1 -* FROM FILE: S:\path\to\ZZ100_501.take_1.0001.exr +* FROM FILE: S:/path/to/ZZ100_501.take_1.0001.exr 002 ZZ100_50 V C 01:00:06:13 01:00:08:15 00:59:54:18 00:59:56:20 * FROM CLIP NAME: take_2 -* FROM FILE: S:\path\to\ZZ100_502A.take_2.0101.exr +* FROM FILE: S:/path/to/ZZ100_502A.take_2.0101.exr ''' timeline = otio.adapters.read_from_string(sample_data, adapter_name="cmx_3600") @@ -1072,11 +1186,13 @@ def test_can_read_frame_cut_points(self): # VALIDATE self.assertEqual(tl.duration().value, 276) - self.assertEqual(len(tl.tracks[0]), 3) - self.assertEqual(tl.tracks[0][0].duration().value, 70) - self.assertEqual(tl.tracks[0][1].in_offset.value, 13) - self.assertEqual(tl.tracks[0][1].out_offset.value, 14) - self.assertEqual(tl.tracks[0][2].duration().value, 206) + self.assertEqual(len(tl.tracks[0]), 4) + self.assertEqual(tl.tracks[0][0].duration().value, 57) + self.assertEqual(tl.tracks[0][0].visible_range().duration.value, 57 + 27) + self.assertEqual(tl.tracks[0][1].in_offset.value, 0) + self.assertEqual(tl.tracks[0][1].out_offset.value, 27) + self.assertEqual(tl.tracks[0][2].duration().value, 27) + self.assertEqual(tl.tracks[0][3].duration().value, 276 - 84) def test_speed_effects(self): tl = otio.adapters.read_from_file( @@ -1163,16 +1279,17 @@ def test_three_part_transition(self): tl = otio.adapters.read_from_file(DISSOLVE_TEST_4) self.assertEqual(len(tl.tracks[0]), 8) + self.assertEqual(tl.tracks[0][0].duration().value, 30.0) + self.assertEqual(tl.tracks[0][1].duration().value, 51.0) + self.assertEqual(tl.tracks[0][1].visible_range().duration.value, 51 + 35) self.assertIsInstance(tl.tracks[0][2], otio.schema.Transition) - self.assertIsInstance(tl.tracks[0][4], otio.schema.Transition) - self.assertEqual(tl.tracks[0][2].duration().value, 35.0) + self.assertEqual(tl.tracks[0][3].duration().value, 81.0) + self.assertEqual(tl.tracks[0][3].visible_range().duration.value, 81 + 64) + self.assertIsInstance(tl.tracks[0][4], otio.schema.Transition) self.assertEqual(tl.tracks[0][4].duration().value, 64.0) - - self.assertEqual(tl.tracks[0][0].duration().value, 30.0) - self.assertEqual(tl.tracks[0][1].duration().value, 68.0) - self.assertEqual(tl.tracks[0][3].duration().value, 96.0) - self.assertEqual(tl.tracks[0][5].duration().value, 52.0) + self.assertEqual(tl.tracks[0][5].duration().value, 84.0) + self.assertEqual(tl.tracks[0][5].visible_range().duration.value, 84.0) self.assertEqual(tl.tracks[0][6].duration().value, 96.0) self.assertEqual(tl.tracks[0][7].duration().value, 135.0) @@ -1199,7 +1316,7 @@ def test_enabled(self): self.assertMultiLineEqual(result, expected) # Disable first clip in the track - tl.tracks[0].children_if()[0].enabled = False + tl.tracks[0].find_children()[0].enabled = False result = otio.adapters.write_to_string(tl, adapter_name="cmx_3600") expected = r'''TITLE: enable_test diff --git a/tests/test_composable.py b/tests/test_composable.py index 1786bf966..e923ebdc6 100644 --- a/tests/test_composable.py +++ b/tests/test_composable.py @@ -1,26 +1,5 @@ -# +# SPDX-License-Identifier: Apache-2.0 # Copyright Contributors to the OpenTimelineIO project -# -# Licensed under the Apache License, Version 2.0 (the "Apache License") -# with the following modification; you may not use this file except in -# compliance with the Apache License and the following modification to it: -# Section 6. Trademarks. is deleted and replaced with: -# -# 6. Trademarks. This License does not grant permission to use the trade -# names, trademarks, service marks, or product names of the Licensor -# and its affiliates, except as required to comply with Section 4(c) of -# the License and to reproduce the content of the NOTICE file. -# -# You may obtain a copy of the Apache License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the Apache License with the above modification is -# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -# KIND, either express or implied. See the Apache License for the specific -# language governing permissions and limitations under the Apache License. -# """Test harness for Composable.""" diff --git a/tests/test_composition.py b/tests/test_composition.py index 6a9e69e6e..40d4e9f4b 100755 --- a/tests/test_composition.py +++ b/tests/test_composition.py @@ -1,27 +1,7 @@ #!/usr/bin/env python # +# SPDX-License-Identifier: Apache-2.0 # Copyright Contributors to the OpenTimelineIO project -# -# Licensed under the Apache License, Version 2.0 (the "Apache License") -# with the following modification; you may not use this file except in -# compliance with the Apache License and the following modification to it: -# Section 6. Trademarks. is deleted and replaced with: -# -# 6. Trademarks. This License does not grant permission to use the trade -# names, trademarks, service marks, or product names of the Licensor -# and its affiliates, except as required to comply with Section 4(c) of -# the License and to reproduce the content of the NOTICE file. -# -# You may obtain a copy of the Apache License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the Apache License with the above modification is -# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -# KIND, either express or implied. See the Apache License for the specific -# language governing permissions and limitations under the Apache License. -# import unittest import os @@ -50,9 +30,9 @@ def test_iterable(self): self.assertEqual([i for i in co], [it]) self.assertEqual(len(co), 1) - self.assertEqual(list(co.each_child()), [it]) + self.assertEqual(list(co.find_children()), [it]) self.assertEqual( - list(co.each_child(descended_from_type=otio.schema.Clip)), + list(co.find_children(descended_from_type=otio.schema.Clip)), [] ) @@ -144,7 +124,7 @@ def test_move_child(self): co2.append(it) self.assertIs(it.parent(), co2) - def test_each_child_recursion(self): + def test_find_children_recursion(self): tl = otio.schema.Timeline(name="TL") tr1 = otio.schema.Track(name="tr1") @@ -180,13 +160,13 @@ def test_each_child_recursion(self): self.assertEqual(2, len(st)) self.assertEqual(2, len(tr3)) - clips = list(tl.each_clip()) + clips = list(tl.find_clips()) self.assertListEqual( [c1, c2, c3, c4, c5, c6, c7, c8], clips ) - all_tracks = list(tl.each_child( + all_tracks = list(tl.find_children( descended_from_type=otio.schema.Track )) self.assertListEqual( @@ -194,7 +174,7 @@ def test_each_child_recursion(self): all_tracks ) - all_stacks = list(tl.each_child( + all_stacks = list(tl.find_children( descended_from_type=otio.schema.Stack )) self.assertListEqual( @@ -202,13 +182,13 @@ def test_each_child_recursion(self): all_stacks ) - all_children = list(tl.each_child()) + all_children = list(tl.find_children()) self.assertListEqual( [tr1, c1, c2, c3, tr2, c4, c5, st, c6, tr3, c7, c8], all_children ) - def test_each_child_options(self): + def test_find_children_options(self): tl = otio.schema.Timeline(name="tl") tr = otio.schema.Track(name="tr") tl.tracks.append(tr) @@ -249,7 +229,7 @@ def test_each_child_options(self): self.assertListEqual( [c1], list( - tr.each_child( + tr.find_children( search_range=otio.opentime.TimeRange( start_time=otio.opentime.RationalTime(value=0, rate=24), duration=otio.opentime.RationalTime(value=50, rate=24) @@ -260,7 +240,7 @@ def test_each_child_options(self): self.assertListEqual( [c2], list( - tr.each_child( + tr.find_children( search_range=otio.opentime.TimeRange( start_time=otio.opentime.RationalTime(value=50, rate=24), duration=otio.opentime.RationalTime(value=50, rate=24) @@ -271,7 +251,7 @@ def test_each_child_options(self): self.assertListEqual( [c1, c2], list( - tr.each_child( + tr.find_children( search_range=otio.opentime.TimeRange( start_time=otio.opentime.RationalTime(value=0, rate=24), duration=otio.opentime.RationalTime(value=100, rate=24) @@ -282,7 +262,7 @@ def test_each_child_options(self): self.assertListEqual( [c1, c2, st, c3], list( - tr.each_child( + tr.find_children( search_range=otio.opentime.TimeRange( start_time=otio.opentime.RationalTime(value=25, rate=24), duration=otio.opentime.RationalTime(value=100, rate=24) @@ -294,18 +274,18 @@ def test_each_child_options(self): # Test descended from type self.assertListEqual( [c1, c2, c3], - list(tl.each_child(descended_from_type=otio.schema.Clip)) + list(tl.find_children(descended_from_type=otio.schema.Clip)) ) self.assertListEqual( [st], - list(tl.each_child(descended_from_type=otio.schema.Stack)) + list(tl.find_children(descended_from_type=otio.schema.Stack)) ) # Test shallow search self.assertListEqual( [c1, c2], list( - tr.each_child( + tr.find_children( descended_from_type=otio.schema.Clip, shallow_search=True ) @@ -1389,11 +1369,11 @@ def test_range_nested(self): self.assertListEqual( [ outer_track.range_of_child(clip) - for clip in outer_track.each_clip() + for clip in outer_track.find_clips() ], [ long_track.range_of_child(clip) - for clip in long_track.each_clip() + for clip in long_track.find_clips() ] ) @@ -1476,7 +1456,7 @@ def test_transformed_time(self): self.assertEqual( list( - sq.each_clip( + sq.find_clips( otio.opentime.TimeRange( otio.opentime.RationalTime(-1, 24) ) @@ -1486,7 +1466,7 @@ def test_transformed_time(self): ) self.assertEqual( list( - sq.each_clip( + sq.find_clips( otio.opentime.TimeRange( otio.opentime.RationalTime(0, 24) ) @@ -1496,7 +1476,7 @@ def test_transformed_time(self): ) self.assertEqual( list( - sq.each_clip( + sq.find_clips( otio.opentime.TimeRange( otio.opentime.RationalTime(49, 24) ) @@ -1506,7 +1486,7 @@ def test_transformed_time(self): ) self.assertEqual( list( - sq.each_clip( + sq.find_clips( otio.opentime.TimeRange( otio.opentime.RationalTime(50, 24) ) @@ -1516,7 +1496,7 @@ def test_transformed_time(self): ) self.assertEqual( list( - sq.each_clip( + sq.find_clips( otio.opentime.TimeRange( otio.opentime.RationalTime(99, 24) ) @@ -1526,7 +1506,7 @@ def test_transformed_time(self): ) self.assertEqual( list( - sq.each_clip( + sq.find_clips( otio.opentime.TimeRange( otio.opentime.RationalTime(100, 24) ) @@ -1536,7 +1516,7 @@ def test_transformed_time(self): ) self.assertEqual( list( - sq.each_clip( + sq.find_clips( otio.opentime.TimeRange( otio.opentime.RationalTime(149, 24) ) @@ -1546,7 +1526,7 @@ def test_transformed_time(self): ) self.assertEqual( list( - sq.each_clip( + sq.find_clips( otio.opentime.TimeRange( otio.opentime.RationalTime(150, 24) ) @@ -1713,7 +1693,7 @@ def test_track_range_of_all_children(self): mp = tr.range_of_all_children() # fetch all the valid children that should be in the map - vc = list(tr.each_clip()) + vc = list(tr.find_clips()) self.assertEqual(mp[vc[0]].start_time.value, 0) self.assertEqual(mp[vc[1]].start_time, mp[vc[0]].duration) @@ -1777,7 +1757,7 @@ def test_iterating_over_dupes(self): # test recursive iteration previous = None - for item in track.each_clip(): + for item in track.find_clips(): self.assertEqual( track.range_of_child(item), item.range_in_parent() @@ -1807,7 +1787,7 @@ def test_iterating_over_dupes(self): # compare recursive to iteration by index previous = None - for i, item in enumerate(track.each_clip()): + for i, item in enumerate(track.find_clips()): self.assertEqual( track.range_of_child(item), track.range_of_child_at_index(i) @@ -2125,13 +2105,13 @@ def test_child_at_time_with_children(self): "got {}".format(playhead, expected_val, measured_val) ) - # then test each_child + # then test find_clips search_range = otio.opentime.TimeRange( otio.opentime.RationalTime(frame, 24), # with a 0 duration, should have the same result as above ) - item = list(sq.each_clip(search_range))[0] + item = list(sq.find_clips(search_range))[0] mediaframe = sq.transformed_time(playhead, item) measured_val = (item.name, otio.opentime.to_frames(mediaframe, 24)) diff --git a/tests/test_console.py b/tests/test_console.py index 3fd860158..a1e2b71c2 100755 --- a/tests/test_console.py +++ b/tests/test_console.py @@ -1,26 +1,5 @@ -# +# SPDX-License-Identifier: Apache-2.0 # Copyright Contributors to the OpenTimelineIO project -# -# Licensed under the Apache License, Version 2.0 (the "Apache License") -# with the following modification; you may not use this file except in -# compliance with the Apache License and the following modification to it: -# Section 6. Trademarks. is deleted and replaced with: -# -# 6. Trademarks. This License does not grant permission to use the trade -# names, trademarks, service marks, or product names of the Licensor -# and its affiliates, except as required to comply with Section 4(c) of -# the License and to reproduce the content of the NOTICE file. -# -# You may obtain a copy of the Apache License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the Apache License with the above modification is -# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -# KIND, either express or implied. See the Apache License for the specific -# language governing permissions and limitations under the Apache License. -# """Unit tests for the 'console' module.""" @@ -29,30 +8,25 @@ import os import subprocess import sysconfig +import pathlib import platform -try: - # python2 - import StringIO as io -except ImportError: - # python3 - import io +import io - -# handle python2 vs python3 difference -try: - from tempfile import TemporaryDirectory # noqa: F401 - import tempfile -except ImportError: - # XXX: python2.7 only - from backports import tempfile +from tempfile import TemporaryDirectory # noqa: F401 +import tempfile import opentimelineio as otio import opentimelineio.test_utils as otio_test_utils import opentimelineio.console as otio_console SAMPLE_DATA_DIR = os.path.join(os.path.dirname(__file__), "sample_data") + +MULTITRACK_PATH = os.path.join(SAMPLE_DATA_DIR, "multitrack.otio") +PREMIERE_EXAMPLE_PATH = os.path.join(SAMPLE_DATA_DIR, "premiere_example.xml") SCREENING_EXAMPLE_PATH = os.path.join(SAMPLE_DATA_DIR, "screening_example.edl") +SIMPLE_CUT_PATH = os.path.join(SAMPLE_DATA_DIR, "simple_cut.otio") +TRANSITION_PATH = os.path.join(SAMPLE_DATA_DIR, "transition.otio") def CreateShelloutTest(cl): @@ -115,6 +89,17 @@ def run_test(self): else: self.test_module.main() + # pre-fetch these strings for easy access + stdout = sys.stdout.getvalue() + stderr = sys.stderr.getvalue() + + if platform.system() == 'Windows': + # Normalize line-endings for assertEqual(expected, actual) + stdout = stdout.replace('\r\n', '\n') + stderr = stderr.replace('\r\n', '\n') + + return stdout, stderr + def tearDown(self): sys.stdout = self.old_stdout sys.stderr = self.old_stderr @@ -193,7 +178,7 @@ def test_basic(self): self.run_test() # read results back in - with open(temp_file, 'r') as fi: + with open(temp_file) as fi: self.assertIn('"name": "Example_Screening.01",', fi.read()) def test_begin_end(self): @@ -331,5 +316,614 @@ def test_basic(self): OTIOPlugInfoTest_ShellOut = CreateShelloutTest(OTIOStatTest) +class OTIOToolTest(ConsoleTester, unittest.TestCase): + test_module = otio_console.otiotool + + def test_list_tracks(self): + sys.argv = [ + 'otiotool', + '-i', MULTITRACK_PATH, + '--list-tracks' + ] + out, err = self.run_test() + self.assertEqual("""TIMELINE: OTIO TEST - multitrack.Exported.01 +TRACK: Sequence (Video) +TRACK: Sequence 2 (Video) +TRACK: Sequence 3 (Video) +""", out) + + def test_list_clips(self): + sys.argv = [ + 'otiotool', + '-i', SCREENING_EXAMPLE_PATH, + '--list-clips' + ] + out, err = self.run_test() + self.assertEqual("""TIMELINE: Example_Screening.01 + CLIP: ZZ100_501 (LAY3) + CLIP: ZZ100_502A (LAY3) + CLIP: ZZ100_503A (LAY1) + CLIP: ZZ100_504C (LAY1) + CLIP: ZZ100_504B (LAY1) + CLIP: ZZ100_507C (LAY2) + CLIP: ZZ100_508 (LAY2) + CLIP: ZZ100_510 (LAY1) + CLIP: ZZ100_510B (LAY1) +""", out) + + def test_list_markers(self): + sys.argv = [ + 'otiotool', + '-i', PREMIERE_EXAMPLE_PATH, + '--list-markers' + ] + out, err = self.run_test() + self.assertEqual( + ("TIMELINE: sc01_sh010_layerA\n" + " MARKER: global: 00:00:03:23 local: 00:00:03:23 duration: 0.0 color: RED name: My MArker 1\n" # noqa: E501 line too long + " MARKER: global: 00:00:16:12 local: 00:00:16:12 duration: 0.0 color: RED name: dsf\n" # noqa: E501 line too long + " MARKER: global: 00:00:09:28 local: 00:00:09:28 duration: 0.0 color: RED name: \n" # noqa: E501 line too long + " MARKER: global: 00:00:13:05 local: 00:00:02:13 duration: 0.0 color: RED name: \n"), # noqa: E501 line too long + out) + + def test_list_tracks_and_clips(self): + sys.argv = [ + 'otiotool', + '-i', MULTITRACK_PATH, + '--list-tracks', + '--list-clips' + ] + out, err = self.run_test() + self.assertEqual("""TIMELINE: OTIO TEST - multitrack.Exported.01 +TRACK: Sequence (Video) + CLIP: tech.fux (loop)-HD.mp4 + CLIP: out-b (loop)-HD.mp4 + CLIP: brokchrd (loop)-HD.mp4 +TRACK: Sequence 2 (Video) + CLIP: t-hawk (loop)-HD.mp4 +TRACK: Sequence 3 (Video) + CLIP: KOLL-HD.mp4 +""", out) + + def test_list_tracks_and_clips_and_media(self): + sys.argv = [ + 'otiotool', + '-i', MULTITRACK_PATH, + '--list-tracks', + '--list-clips', + '--list-media' + ] + out, err = self.run_test() + self.assertEqual("""TIMELINE: OTIO TEST - multitrack.Exported.01 +TRACK: Sequence (Video) + CLIP: tech.fux (loop)-HD.mp4 + MEDIA: None + CLIP: out-b (loop)-HD.mp4 + MEDIA: None + CLIP: brokchrd (loop)-HD.mp4 + MEDIA: None +TRACK: Sequence 2 (Video) + CLIP: t-hawk (loop)-HD.mp4 + MEDIA: None +TRACK: Sequence 3 (Video) + CLIP: KOLL-HD.mp4 + MEDIA: None +""", out) + + def test_list_tracks_and_clips_and_media_and_markers(self): + sys.argv = [ + 'otiotool', + '-i', PREMIERE_EXAMPLE_PATH, + '--list-tracks', + '--list-clips', + '--list-media', + '--list-markers' + ] + out, err = self.run_test() + self.assertEqual( + ("TIMELINE: sc01_sh010_layerA\n" + " MARKER: global: 00:00:03:23 local: 00:00:03:23 duration: 0.0 color: RED name: My MArker 1\n" # noqa E501 line too long + " MARKER: global: 00:00:16:12 local: 00:00:16:12 duration: 0.0 color: RED name: dsf\n" # noqa E501 line too long + " MARKER: global: 00:00:09:28 local: 00:00:09:28 duration: 0.0 color: RED name: \n" # noqa E501 line too long + "TRACK: (Video)\n" + " CLIP: sc01_sh010_anim.mov\n" + " MEDIA: file://localhost/D%3a/media/sc01_sh010_anim.mov\n" + "TRACK: (Video)\n" + " CLIP: sc01_sh010_anim.mov\n" + " MEDIA: file://localhost/D%3a/media/sc01_sh010_anim.mov\n" + " CLIP: sc01_sh020_anim.mov\n" + " MEDIA: file://localhost/D%3a/media/sc01_sh020_anim.mov\n" + " CLIP: sc01_sh030_anim.mov\n" + " MEDIA: file://localhost/D%3a/media/sc01_sh030_anim.mov\n" + " MARKER: global: 00:00:13:05 local: 00:00:02:13 duration: 0.0 color: RED name: \n" # noqa E501 line too long + "TRACK: (Video)\n" + " CLIP: test_title\n" + " MEDIA: None\n" + "TRACK: (Video)\n" + " CLIP: sc01_master_layerA_sh030_temp.mov\n" + " MEDIA: file://localhost/D%3a/media/sc01_master_layerA_sh030_temp.mov\n" # noqa E501 line too long + " CLIP: sc01_sh010_anim.mov\n" + " MEDIA: file://localhost/D%3a/media/sc01_sh010_anim.mov\n" + "TRACK: (Audio)\n" + " CLIP: sc01_sh010_anim.mov\n" + " MEDIA: file://localhost/D%3a/media/sc01_sh010_anim.mov\n" + " CLIP: sc01_sh010_anim.mov\n" + " MEDIA: file://localhost/D%3a/media/sc01_sh010_anim.mov\n" + "TRACK: (Audio)\n" + " CLIP: sc01_placeholder.wav\n" + " MEDIA: file://localhost/D%3a/media/sc01_placeholder.wav\n" + "TRACK: (Audio)\n" + " CLIP: track_08.wav\n" + " MEDIA: file://localhost/D%3a/media/track_08.wav\n" + "TRACK: (Audio)\n" + " CLIP: sc01_master_layerA_sh030_temp.mov\n" + " MEDIA: file://localhost/D%3a/media/sc01_master_layerA_sh030_temp.mov\n" # noqa E501 line too long + " CLIP: sc01_sh010_anim.mov\n" + " MEDIA: file://localhost/D%3a/media/sc01_sh010_anim.mov\n"), + out) + + def test_verify_media(self): + sys.argv = [ + 'otiotool', + '-i', PREMIERE_EXAMPLE_PATH, + '--list-tracks', + '--list-clips', + '--list-media', + '--verify-media' + ] + out, err = self.run_test() + self.assertEqual("""TIMELINE: sc01_sh010_layerA +TRACK: (Video) + CLIP: sc01_sh010_anim.mov + MEDIA NOT FOUND: file://localhost/D%3a/media/sc01_sh010_anim.mov +TRACK: (Video) + CLIP: sc01_sh010_anim.mov + MEDIA NOT FOUND: file://localhost/D%3a/media/sc01_sh010_anim.mov + CLIP: sc01_sh020_anim.mov + MEDIA NOT FOUND: file://localhost/D%3a/media/sc01_sh020_anim.mov + CLIP: sc01_sh030_anim.mov + MEDIA NOT FOUND: file://localhost/D%3a/media/sc01_sh030_anim.mov +TRACK: (Video) + CLIP: test_title + MEDIA: None +TRACK: (Video) + CLIP: sc01_master_layerA_sh030_temp.mov + MEDIA NOT FOUND: file://localhost/D%3a/media/sc01_master_layerA_sh030_temp.mov + CLIP: sc01_sh010_anim.mov + MEDIA NOT FOUND: file://localhost/D%3a/media/sc01_sh010_anim.mov +TRACK: (Audio) + CLIP: sc01_sh010_anim.mov + MEDIA NOT FOUND: file://localhost/D%3a/media/sc01_sh010_anim.mov + CLIP: sc01_sh010_anim.mov + MEDIA NOT FOUND: file://localhost/D%3a/media/sc01_sh010_anim.mov +TRACK: (Audio) + CLIP: sc01_placeholder.wav + MEDIA NOT FOUND: file://localhost/D%3a/media/sc01_placeholder.wav +TRACK: (Audio) + CLIP: track_08.wav + MEDIA NOT FOUND: file://localhost/D%3a/media/track_08.wav +TRACK: (Audio) + CLIP: sc01_master_layerA_sh030_temp.mov + MEDIA NOT FOUND: file://localhost/D%3a/media/sc01_master_layerA_sh030_temp.mov + CLIP: sc01_sh010_anim.mov + MEDIA NOT FOUND: file://localhost/D%3a/media/sc01_sh010_anim.mov +""", out) + + def test_video_only(self): + sys.argv = [ + 'otiotool', + '-i', PREMIERE_EXAMPLE_PATH, + '--video-only', + '--list-clips' + ] + out, err = self.run_test() + self.assertEqual("""TIMELINE: sc01_sh010_layerA + CLIP: sc01_sh010_anim.mov + CLIP: sc01_sh010_anim.mov + CLIP: sc01_sh020_anim.mov + CLIP: sc01_sh030_anim.mov + CLIP: test_title + CLIP: sc01_master_layerA_sh030_temp.mov + CLIP: sc01_sh010_anim.mov +""", out) + + def test_audio_only(self): + sys.argv = [ + 'otiotool', + '-i', PREMIERE_EXAMPLE_PATH, + '--audio-only', + '--list-clips' + ] + out, err = self.run_test() + self.assertEqual("""TIMELINE: sc01_sh010_layerA + CLIP: sc01_sh010_anim.mov + CLIP: sc01_sh010_anim.mov + CLIP: sc01_placeholder.wav + CLIP: track_08.wav + CLIP: sc01_master_layerA_sh030_temp.mov + CLIP: sc01_sh010_anim.mov +""", out) + + def test_only_tracks_with_name(self): + sys.argv = [ + 'otiotool', + '-i', MULTITRACK_PATH, + '--only-tracks-with-name', 'Sequence 3', + '--list-clips' + ] + out, err = self.run_test() + self.assertEqual("""TIMELINE: OTIO TEST - multitrack.Exported.01 + CLIP: KOLL-HD.mp4 +""", out) + + def test_only_tracks_with_index(self): + sys.argv = [ + 'otiotool', + '-i', MULTITRACK_PATH, + '--only-tracks-with-index', '3', + '--list-clips' + ] + out, err = self.run_test() + self.assertEqual("""TIMELINE: OTIO TEST - multitrack.Exported.01 + CLIP: KOLL-HD.mp4 +""", out) + + def test_only_tracks_with_index2(self): + sys.argv = [ + 'otiotool', + '-i', MULTITRACK_PATH, + '--only-tracks-with-index', '2', '3', + '--list-clips' + ] + out, err = self.run_test() + self.assertEqual("""TIMELINE: OTIO TEST - multitrack.Exported.01 + CLIP: t-hawk (loop)-HD.mp4 + CLIP: KOLL-HD.mp4 +""", out) + + def test_only_clips_with_name(self): + sys.argv = [ + 'otiotool', + '-i', PREMIERE_EXAMPLE_PATH, + '--list-clips', + '--list-tracks', + '--only-clips-with-name', 'sc01_sh010_anim.mov' + ] + out, err = self.run_test() + self.assertEqual("""TIMELINE: sc01_sh010_layerA +TRACK: (Video) + CLIP: sc01_sh010_anim.mov +TRACK: (Video) + CLIP: sc01_sh010_anim.mov +TRACK: (Video) +TRACK: (Video) + CLIP: sc01_sh010_anim.mov +TRACK: (Audio) + CLIP: sc01_sh010_anim.mov + CLIP: sc01_sh010_anim.mov +TRACK: (Audio) +TRACK: (Audio) +TRACK: (Audio) + CLIP: sc01_sh010_anim.mov +""", out) + + def test_only_clips_with_regex(self): + sys.argv = [ + 'otiotool', + '-i', PREMIERE_EXAMPLE_PATH, + '--list-clips', + '--list-tracks', + '--only-clips-with-name-regex', 'anim' + ] + out, err = self.run_test() + self.assertEqual("""TIMELINE: sc01_sh010_layerA +TRACK: (Video) + CLIP: sc01_sh010_anim.mov +TRACK: (Video) + CLIP: sc01_sh010_anim.mov + CLIP: sc01_sh020_anim.mov + CLIP: sc01_sh030_anim.mov +TRACK: (Video) +TRACK: (Video) + CLIP: sc01_sh010_anim.mov +TRACK: (Audio) + CLIP: sc01_sh010_anim.mov + CLIP: sc01_sh010_anim.mov +TRACK: (Audio) +TRACK: (Audio) +TRACK: (Audio) + CLIP: sc01_sh010_anim.mov +""", out) + + def test_remote_transition(self): + sys.argv = [ + 'otiotool', + '-i', TRANSITION_PATH, + '-o', '-', + '--remove-transitions' + ] + out, err = self.run_test() + self.assertNotIn('"OTIO_SCHEMA": "Transition.', out) + + def test_trim(self): + sys.argv = [ + 'otiotool', + '-i', MULTITRACK_PATH, + '--trim', '20', '40', + '--list-clips', + '--inspect', 't-hawk' + ] + out, err = self.run_test() + self.assertEqual( + ("TIMELINE: OTIO TEST - multitrack.Exported.01\n" + " ITEM: t-hawk (loop)-HD.mp4 ()\n" # noqa E501 line too long + " source_range: TimeRange(RationalTime(0, 24), RationalTime(478, 24))\n" # noqa E501 line too long + " trimmed_range: TimeRange(RationalTime(0, 24), RationalTime(478, 24))\n" # noqa E501 line too long + " visible_range: TimeRange(RationalTime(0, 24), RationalTime(478, 24))\n" # noqa E501 line too long + " range_in_parent: TimeRange(RationalTime(2, 24), RationalTime(478, 24))\n" # noqa E501 line too long + " trimmed range in timeline: TimeRange(RationalTime(2, 24), RationalTime(478, 24))\n" # noqa E501 line too long + " visible range in timeline: TimeRange(RationalTime(2, 24), RationalTime(478, 24))\n" # noqa E501 line too long + " range in Sequence 2 (): TimeRange(RationalTime(2, 24), RationalTime(478, 24))\n" # noqa E501 line too long + " range in NestedScope (): TimeRange(RationalTime(2, 24), RationalTime(478, 24))\n" # noqa E501 line too long + "TIMELINE: OTIO TEST - multitrack.Exported.01\n" + " CLIP: tech.fux (loop)-HD.mp4\n" + " CLIP: out-b (loop)-HD.mp4\n" + " CLIP: t-hawk (loop)-HD.mp4\n"), + out) + + def test_flatten(self): + sys.argv = [ + 'otiotool', + '-i', MULTITRACK_PATH, + '--flatten', 'video', + '--list-clips', + '--list-tracks', + '--inspect', 'out-b' + ] + out, err = self.run_test() + self.assertEqual( + ("TIMELINE: OTIO TEST - multitrack.Exported.01\n" + " ITEM: out-b (loop)-HD.mp4 ()\n" # noqa E501 line too long + " source_range: TimeRange(RationalTime(159, 24), RationalTime(236, 24))\n" # noqa E501 line too long + " trimmed_range: TimeRange(RationalTime(159, 24), RationalTime(236, 24))\n" # noqa E501 line too long + " visible_range: TimeRange(RationalTime(159, 24), RationalTime(236, 24))\n" # noqa E501 line too long + " range_in_parent: TimeRange(RationalTime(962, 24), RationalTime(236, 24))\n" # noqa E501 line too long + " trimmed range in timeline: TimeRange(RationalTime(962, 24), RationalTime(236, 24))\n" # noqa E501 line too long + " visible range in timeline: TimeRange(RationalTime(962, 24), RationalTime(236, 24))\n" # noqa E501 line too long + " range in Flattened (): TimeRange(RationalTime(962, 24), RationalTime(236, 24))\n" # noqa E501 line too long + " range in NestedScope (): TimeRange(RationalTime(962, 24), RationalTime(236, 24))\n" # noqa E501 line too long + "TIMELINE: OTIO TEST - multitrack.Exported.01\n" + "TRACK: Flattened (Video)\n" + " CLIP: tech.fux (loop)-HD.mp4\n" + " CLIP: t-hawk (loop)-HD.mp4\n" + " CLIP: out-b (loop)-HD.mp4\n" + " CLIP: KOLL-HD.mp4\n" + " CLIP: brokchrd (loop)-HD.mp4\n"), + out) + + def test_keep_flattened_tracks(self): + sys.argv = [ + 'otiotool', + '-i', MULTITRACK_PATH, + '--flatten', 'video', + '--keep-flattened-tracks', + '--list-clips', + '--list-tracks', + '--inspect', 'out-b' + ] + out, err = self.run_test() + self.assertEqual( + ("TIMELINE: OTIO TEST - multitrack.Exported.01\n" + " ITEM: out-b (loop)-HD.mp4 ()\n" # noqa E501 line too long + " source_range: TimeRange(RationalTime(0, 24), RationalTime(722, 24))\n" # noqa E501 line too long + " trimmed_range: TimeRange(RationalTime(0, 24), RationalTime(722, 24))\n" # noqa E501 line too long + " visible_range: TimeRange(RationalTime(0, 24), RationalTime(722, 24))\n" # noqa E501 line too long + " range_in_parent: TimeRange(RationalTime(803, 24), RationalTime(722, 24))\n" # noqa E501 line too long + " trimmed range in timeline: TimeRange(RationalTime(803, 24), RationalTime(722, 24))\n" # noqa E501 line too long + " visible range in timeline: TimeRange(RationalTime(803, 24), RationalTime(722, 24))\n" # noqa E501 line too long + " range in Sequence (): TimeRange(RationalTime(803, 24), RationalTime(722, 24))\n" # noqa E501 line too long + " range in NestedScope (): TimeRange(RationalTime(803, 24), RationalTime(722, 24))\n" # noqa E501 line too long + " ITEM: out-b (loop)-HD.mp4 ()\n" # noqa E501 line too long + " source_range: TimeRange(RationalTime(159, 24), RationalTime(236, 24))\n" # noqa E501 line too long + " trimmed_range: TimeRange(RationalTime(159, 24), RationalTime(236, 24))\n" # noqa E501 line too long + " visible_range: TimeRange(RationalTime(159, 24), RationalTime(236, 24))\n" # noqa E501 line too long + " range_in_parent: TimeRange(RationalTime(962, 24), RationalTime(236, 24))\n" # noqa E501 line too long + " trimmed range in timeline: TimeRange(RationalTime(962, 24), RationalTime(236, 24))\n" # noqa E501 line too long + " visible range in timeline: TimeRange(RationalTime(962, 24), RationalTime(236, 24))\n" # noqa E501 line too long + " range in Flattened (): TimeRange(RationalTime(962, 24), RationalTime(236, 24))\n" # noqa E501 line too long + " range in NestedScope (): TimeRange(RationalTime(962, 24), RationalTime(236, 24))\n" # noqa E501 line too long + "TIMELINE: OTIO TEST - multitrack.Exported.01\n" + "TRACK: Sequence (Video)\n" + " CLIP: tech.fux (loop)-HD.mp4\n" + " CLIP: out-b (loop)-HD.mp4\n" + " CLIP: brokchrd (loop)-HD.mp4\n" + "TRACK: Sequence 2 (Video)\n" + " CLIP: t-hawk (loop)-HD.mp4\n" + "TRACK: Sequence 3 (Video)\n" + " CLIP: KOLL-HD.mp4\n" + "TRACK: Flattened (Video)\n" + " CLIP: tech.fux (loop)-HD.mp4\n" + " CLIP: t-hawk (loop)-HD.mp4\n" + " CLIP: out-b (loop)-HD.mp4\n" + " CLIP: KOLL-HD.mp4\n" + " CLIP: brokchrd (loop)-HD.mp4\n"), + out) + + def test_stack(self): + sys.argv = [ + 'otiotool', + '-i', MULTITRACK_PATH, PREMIERE_EXAMPLE_PATH, + '--stack', + '--list-clips', + '--list-tracks', + '--stats' + ] + out, err = self.run_test() + self.maxDiff = None + self.assertEqual("""Name: Stacked 2 Timelines +Start: 00:00:00:00 +End: 00:02:16:18 +Duration: 00:02:16:18 +TIMELINE: Stacked 2 Timelines +TRACK: Sequence (Video) + CLIP: tech.fux (loop)-HD.mp4 + CLIP: out-b (loop)-HD.mp4 + CLIP: brokchrd (loop)-HD.mp4 +TRACK: Sequence 2 (Video) + CLIP: t-hawk (loop)-HD.mp4 +TRACK: Sequence 3 (Video) + CLIP: KOLL-HD.mp4 +TRACK: (Video) + CLIP: sc01_sh010_anim.mov +TRACK: (Video) + CLIP: sc01_sh010_anim.mov + CLIP: sc01_sh020_anim.mov + CLIP: sc01_sh030_anim.mov +TRACK: (Video) + CLIP: test_title +TRACK: (Video) + CLIP: sc01_master_layerA_sh030_temp.mov + CLIP: sc01_sh010_anim.mov +TRACK: (Audio) + CLIP: sc01_sh010_anim.mov + CLIP: sc01_sh010_anim.mov +TRACK: (Audio) + CLIP: sc01_placeholder.wav +TRACK: (Audio) + CLIP: track_08.wav +TRACK: (Audio) + CLIP: sc01_master_layerA_sh030_temp.mov + CLIP: sc01_sh010_anim.mov +""", out) + + def test_concat(self): + sys.argv = [ + 'otiotool', + '-i', MULTITRACK_PATH, PREMIERE_EXAMPLE_PATH, + '--concat', + '--list-clips', + '--list-tracks', + '--stats' + ] + out, err = self.run_test() + self.maxDiff = None + self.assertEqual("""Name: Concatenated 2 Timelines +Start: 00:00:00:00 +End: 00:02:59:03 +Duration: 00:02:59:03 +TIMELINE: Concatenated 2 Timelines +TRACK: (Video) +TRACK: Sequence (Video) + CLIP: tech.fux (loop)-HD.mp4 + CLIP: out-b (loop)-HD.mp4 + CLIP: brokchrd (loop)-HD.mp4 +TRACK: Sequence 2 (Video) + CLIP: t-hawk (loop)-HD.mp4 +TRACK: Sequence 3 (Video) + CLIP: KOLL-HD.mp4 +TRACK: (Video) + CLIP: sc01_sh010_anim.mov +TRACK: (Video) + CLIP: sc01_sh010_anim.mov + CLIP: sc01_sh020_anim.mov + CLIP: sc01_sh030_anim.mov +TRACK: (Video) + CLIP: test_title +TRACK: (Video) + CLIP: sc01_master_layerA_sh030_temp.mov + CLIP: sc01_sh010_anim.mov +TRACK: (Audio) + CLIP: sc01_sh010_anim.mov + CLIP: sc01_sh010_anim.mov +TRACK: (Audio) + CLIP: sc01_placeholder.wav +TRACK: (Audio) + CLIP: track_08.wav +TRACK: (Audio) + CLIP: sc01_master_layerA_sh030_temp.mov + CLIP: sc01_sh010_anim.mov +""", out) + + def test_redact(self): + sys.argv = [ + 'otiotool', + '-i', MULTITRACK_PATH, + '--redact', + '--list-clips', + '--list-tracks' + ] + out, err = self.run_test() + self.assertEqual("""TIMELINE: Timeline #1 +TRACK: Track #1 (Video) + CLIP: Clip #1 + CLIP: Clip #2 + CLIP: Clip #3 +TRACK: Track #2 (Video) + CLIP: Clip #4 +TRACK: Track #3 (Video) + CLIP: Clip #5 +""", out) + + def test_stats(self): + sys.argv = [ + 'otiotool', + '-i', MULTITRACK_PATH, + '--stats' + ] + out, err = self.run_test() + self.assertEqual("""Name: OTIO TEST - multitrack.Exported.01 +Start: 00:00:00:00 +End: 00:02:16:18 +Duration: 00:02:16:18 +""", out) + + def test_inspect(self): + sys.argv = [ + 'otiotool', + '-i', MULTITRACK_PATH, + '--inspect', 'KOLL' + ] + out, err = self.run_test() + self.assertEqual( + ("TIMELINE: OTIO TEST - multitrack.Exported.01\n" + " ITEM: KOLL-HD.mp4 ()\n" + " source_range: TimeRange(RationalTime(0, 24), RationalTime(640, 24))\n" # noqa E501 line too long + " trimmed_range: TimeRange(RationalTime(0, 24), RationalTime(640, 24))\n" # noqa E501 line too long + " visible_range: TimeRange(RationalTime(0, 24), RationalTime(640, 24))\n" # noqa E501 line too long + " range_in_parent: TimeRange(RationalTime(1198, 24), RationalTime(640, 24))\n" # noqa E501 line too long + " trimmed range in timeline: TimeRange(RationalTime(1198, 24), RationalTime(640, 24))\n" # noqa E501 line too long + " visible range in timeline: TimeRange(RationalTime(1198, 24), RationalTime(640, 24))\n" # noqa E501 line too long + " range in Sequence 3 (): TimeRange(RationalTime(1198, 24), RationalTime(640, 24))\n" # noqa E501 line too long + " range in NestedScope (): TimeRange(RationalTime(1198, 24), RationalTime(640, 24))\n"), # noqa E501 line too long + out) + + def test_relink(self): + with tempfile.TemporaryDirectory() as temp_dir: + temp_file1 = os.path.join(temp_dir, "Clip-001.empty") + temp_file2 = os.path.join(temp_dir, "Clip-003.empty") + open(temp_file1, "w").write("A") + open(temp_file2, "w").write("B") + + temp_url = pathlib.Path(temp_dir).as_uri() + + sys.argv = [ + 'otiotool', + '-i', SIMPLE_CUT_PATH, + '--relink-by-name', temp_dir, + '--list-media' + ] + out, err = self.run_test() + self.assertIn( + ("TIMELINE: Figure 1 - Simple Cut List\n" + f" MEDIA: {temp_url}/Clip-001.empty\n" + " MEDIA: file:///folder/wind-up.mov\n" + f" MEDIA: {temp_url}/Clip-003.empty\n" + " MEDIA: file:///folder/credits.mov\n"), + out) + + +OTIOToolTest_ShellOut = CreateShelloutTest(OTIOToolTest) + + if __name__ == '__main__': unittest.main() diff --git a/tests/test_core.py b/tests/test_core.py new file mode 100644 index 000000000..04b66cf14 --- /dev/null +++ b/tests/test_core.py @@ -0,0 +1,43 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright Contributors to the OpenTimelineIO project + +import sys +import shutil +import tempfile +import unittest + +import opentimelineio as otio + + +class TestCoreFunctions(unittest.TestCase): + def setUp(self): + self.tmpDir = tempfile.mkdtemp() + + def tearDown(self): + shutil.rmtree(self.tmpDir) + + def test_deserialize_json_from_file_errors(self): + """Verify the bindings return the correct errors based on the errno""" + + with self.assertRaises(FileNotFoundError) as exc: + otio.core.deserialize_json_from_file('non-existent-file-here') + self.assertIsInstance(exc.exception, FileNotFoundError) + + @unittest.skipUnless( + not sys.platform.startswith("win"), + "requires non Windows system" + ) + def test_serialize_json_to_file_errors_non_windows(self): + """Verify the bindings return the correct errors based on the errno""" + + with self.assertRaises(IsADirectoryError) as exc: + otio.core.serialize_json_to_file({}, self.tmpDir) + self.assertIsInstance(exc.exception, IsADirectoryError) + + @unittest.skipUnless(sys.platform.startswith("win"), "requires Windows") + def test_serialize_json_to_file_errors_windows(self): + """Verify the bindings return the correct errors based on the errno""" + + with self.assertRaises(PermissionError) as exc: + otio.core.serialize_json_to_file({}, self.tmpDir) + self.assertIsInstance(exc.exception, PermissionError) diff --git a/tests/test_core_utils.py b/tests/test_core_utils.py new file mode 100644 index 000000000..a0a7b9425 --- /dev/null +++ b/tests/test_core_utils.py @@ -0,0 +1,241 @@ +import copy +import unittest + +import opentimelineio._otio +import opentimelineio.core._core_utils + + +class AnyDictionaryTests(unittest.TestCase): + def test_main(self): + d = opentimelineio.core._core_utils.AnyDictionary() + d['a'] = 1 + + self.assertTrue('a' in d) + self.assertFalse('asdasdasd' in d) + + self.assertEqual(len(d), 1) + self.assertEqual(d['a'], 1) # New key + + with self.assertRaisesRegex(KeyError, "'non-existent'"): + d['non-existent'] + + # TODO: Test different type of values to exercise the any_to_py function? + + d['a'] = 'newvalue' + self.assertEqual(d['a'], 'newvalue') + + self.assertTrue('a' in d) # Test __contains__ + self.assertFalse('b' in d) + + with self.assertRaises(TypeError): + d[1] # AnyDictionary.__getitem__ only supports strings + + del d['a'] + self.assertEqual(dict(d), {}) + with self.assertRaisesRegex(KeyError, "'non-existent'"): + del d['non-existent'] + + for key in iter(d): # Test AnyDictionaryProxy.Iterator.iter + self.assertTrue(key) + + class CustomClass(object): + pass + + with self.assertRaises(TypeError): + d['custom'] = CustomClass() + + with self.assertRaises(ValueError): + # Integer bigger than C++ int64_t can accept. + d['super big int'] = 9223372036854775808 + + with self.assertRaises(ValueError): + # Integer smaller than C++ int64_t can accept. + d['super big int'] = -9223372036854775809 + + def test_raise_on_mutation_during_iter(self): + d = opentimelineio.core._core_utils.AnyDictionary() + d['a'] = 'test' + d['b'] = 'asdasda' + + with self.assertRaisesRegex(ValueError, "container mutated during iteration"): + for key in d: + del d['b'] + + def test_raises_if_ref_destroyed(self): + d1 = opentimelineio.core._core_utils.AnyDictionary() + opentimelineio._otio._testing.test_AnyDictionary_destroy(d1) + + with self.assertRaisesRegex(ValueError, r"Underlying C\+\+ AnyDictionary has been destroyed"): # noqa + d1['asd'] + + d2 = opentimelineio.core._core_utils.AnyDictionary() + opentimelineio._otio._testing.test_AnyDictionary_destroy(d2) + + with self.assertRaisesRegex(ValueError, r"Underlying C\+\+ AnyDictionary has been destroyed"): # noqa + d2['asd'] = 'asd' + + d3 = opentimelineio.core._core_utils.AnyDictionary() + opentimelineio._otio._testing.test_AnyDictionary_destroy(d3) + + with self.assertRaisesRegex(ValueError, r"Underlying C\+\+ AnyDictionary has been destroyed"): # noqa + del d3['asd'] + + d4 = opentimelineio.core._core_utils.AnyDictionary() + d4['asd'] = 1 + it = iter(d4) + opentimelineio._otio._testing.test_AnyDictionary_destroy(d4) + with self.assertRaisesRegex(ValueError, r"Underlying C\+\+ AnyDictionary has been destroyed"): # noqa + next(it) + + +class AnyVectorTests(unittest.TestCase): + def test_main(self): + v = opentimelineio.core._core_utils.AnyVector() + + with self.assertRaises(IndexError): + del v[0] # There is a special case in the C++ code for empty vector + + v.append(1) + self.assertEqual(len(v), 1) + v.append(2) + self.assertEqual(len(v), 2) + + self.assertEqual([value for value in v], [1, 2]) + + v.insert(0, 5) + self.assertEqual([value for value in v], [5, 1, 2]) + self.assertEqual(v[0], 5) + self.assertEqual(v[-3], 5) + + with self.assertRaises(IndexError): + v[100] + + with self.assertRaises(IndexError): + v[-100] + + v[-1] = 100 + self.assertEqual(v[2], 100) + + with self.assertRaises(IndexError): + v[-4] = -1 + + with self.assertRaises(IndexError): + v[100] = 100 + + del v[0] + self.assertEqual(len(v), 2) + # Doesn't work... + # assert v == [1, 100] + self.assertEqual([value for value in v], [1, 100]) + + del v[1000] # This will surprisingly delete the last item... + self.assertEqual(len(v), 1) + self.assertEqual([value for value in v], [1]) + + # Will delete the last item even if the index doesn't match. + # It's a surprising behavior. + # This is caused by size_t(index) + del v[-1000] + + v.extend([1, '234', {}]) + + items = [] + for value in iter(v): # Test AnyVector.Iterator.iter + items.append(value) + + self.assertEqual(items, [1, '234', {}]) + self.assertFalse(v == [1, '234', {}]) # __eq__ is not implemented + + self.assertTrue(1 in v) # Test __contains__ + self.assertTrue('234' in v) + self.assertTrue({} in v) + self.assertFalse(5 in v) + + self.assertEqual(list(reversed(v)), [{}, '234', 1]) + + self.assertEqual(v.index('234'), 1) + + v += [1, 2] + self.assertEqual(v.count(1), 2) + + self.assertEqual(v + ['new'], [1, '234', {}, 1, 2, 'new']) # __add__ + self.assertEqual(['new'] + v, [1, '234', {}, 1, 2, 'new']) # __radd__ + + self.assertEqual(v + ('new',), [1, '234', {}, 1, 2, 'new']) # noqa __add__ with non list type + + v2 = opentimelineio.core._core_utils.AnyVector() + v2.append('v2') + + self.assertEqual(v + v2, [1, '234', {}, 1, 2, 'v2']) # __add__ with AnyVector + + with self.assertRaises(TypeError): + v + 'asd' # __add__ invalid type + + self.assertEqual(str(v), "[1, '234', {}, 1, 2]") + self.assertEqual(repr(v), "[1, '234', {}, 1, 2]") + + v3 = opentimelineio.core._core_utils.AnyVector() + v3.extend(range(10)) + self.assertEqual(v3[2:], [2, 3, 4, 5, 6, 7, 8, 9]) + self.assertEqual(v3[4:8], [4, 5, 6, 7]) + self.assertEqual(v3[1:7:2], [1, 3, 5]) + + del v3[2:7] + self.assertEqual(list(v3), [0, 1, 7, 8, 9]) + + v4 = opentimelineio.core._core_utils.AnyVector() + v4.extend(range(10)) + + del v4[::2] + self.assertEqual(list(v4), [1, 3, 5, 7, 9]) + + v5 = opentimelineio.core._core_utils.AnyVector() + tmplist = [1, 2] + v5.append(tmplist) + # If AnyVector was a pure list, this would fail. But it's not a real list. + # Appending copies data, completely removing references to it. + self.assertIsNot(v5[0], tmplist) + + def test_raises_if_ref_destroyed(self): + v1 = opentimelineio.core._core_utils.AnyVector() + opentimelineio._otio._testing.test_AnyVector_destroy(v1) + + with self.assertRaisesRegex(ValueError, r"Underlying C\+\+ AnyVector object has been destroyed"): # noqa + v1[0] + + v2 = opentimelineio.core._core_utils.AnyVector() + opentimelineio._otio._testing.test_AnyVector_destroy(v2) + + with self.assertRaisesRegex(ValueError, r"Underlying C\+\+ AnyVector object has been destroyed"): # noqa + v2[0] = 1 + + v3 = opentimelineio.core._core_utils.AnyVector() + opentimelineio._otio._testing.test_AnyVector_destroy(v3) + + with self.assertRaisesRegex(ValueError, r"Underlying C\+\+ AnyVector object has been destroyed"): # noqa + del v3[0] + + v4 = opentimelineio.core._core_utils.AnyVector() + v4.append(1) + it = iter(v4) + opentimelineio._otio._testing.test_AnyVector_destroy(v4) + with self.assertRaisesRegex(ValueError, r"Underlying C\+\+ AnyVector object has been destroyed"): # noqa + next(it) + + def test_copy(self): + list1 = [1, 2, [3, 4], 5] + copied = copy.copy(list1) + self.assertEqual(list(list1), list(copied)) + + v = opentimelineio.core._core_utils.AnyVector() + v.extend([1, 2, [3, 4], 5]) + + copied = copy.copy(v) + self.assertIsNot(v, copied) + # AnyVector can only deep copy. So it's __copy__ + # does a deepcopy. + self.assertIsNot(v[2], copied[2]) + + deepcopied = copy.deepcopy(v) + self.assertIsNot(v, deepcopied) + self.assertIsNot(v[2], deepcopied[2]) diff --git a/tests/test_cxx_sdk_bindings.py b/tests/test_cxx_sdk_bindings.py index 7cdf625b8..544651a03 100644 --- a/tests/test_cxx_sdk_bindings.py +++ b/tests/test_cxx_sdk_bindings.py @@ -1,26 +1,5 @@ -# +# SPDX-License-Identifier: Apache-2.0 # Copyright Contributors to the OpenTimelineIO project -# -# Licensed under the Apache License, Version 2.0 (the "Apache License") -# with the following modification; you may not use this file except in -# compliance with the Apache License and the following modification to it: -# Section 6. Trademarks. is deleted and replaced with: -# -# 6. Trademarks. This License does not grant permission to use the trade -# names, trademarks, service marks, or product names of the Licensor -# and its affiliates, except as required to comply with Section 4(c) of -# the License and to reproduce the content of the NOTICE file. -# -# You may obtain a copy of the Apache License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the Apache License with the above modification is -# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -# KIND, either express or implied. See the Apache License for the specific -# language governing permissions and limitations under the Apache License. -# import unittest diff --git a/tests/test_documentation.py b/tests/test_documentation.py index f3ba41f94..2767035fa 100644 --- a/tests/test_documentation.py +++ b/tests/test_documentation.py @@ -1,26 +1,5 @@ -# +# SPDX-License-Identifier: Apache-2.0 # Copyright Contributors to the OpenTimelineIO project -# -# Licensed under the Apache License, Version 2.0 (the "Apache License") -# with the following modification; you may not use this file except in -# compliance with the Apache License and the following modification to it: -# Section 6. Trademarks. is deleted and replaced with: -# -# 6. Trademarks. This License does not grant permission to use the trade -# names, trademarks, service marks, or product names of the Licensor -# and its affiliates, except as required to comply with Section 4(c) of -# the License and to reproduce the content of the NOTICE file. -# -# You may obtain a copy of the Apache License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the Apache License with the above modification is -# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -# KIND, either express or implied. See the Apache License for the specific -# language governing permissions and limitations under the Apache License. -# """Test cases to verify examples used in the OTIO documentation.""" diff --git a/tests/test_effect.py b/tests/test_effect.py index ce5be5fc5..9e615695b 100644 --- a/tests/test_effect.py +++ b/tests/test_effect.py @@ -1,26 +1,5 @@ -# +# SPDX-License-Identifier: Apache-2.0 # Copyright Contributors to the OpenTimelineIO project -# -# Licensed under the Apache License, Version 2.0 (the "Apache License") -# with the following modification; you may not use this file except in -# compliance with the Apache License and the following modification to it: -# Section 6. Trademarks. is deleted and replaced with: -# -# 6. Trademarks. This License does not grant permission to use the trade -# names, trademarks, service marks, or product names of the Licensor -# and its affiliates, except as required to comply with Section 4(c) of -# the License and to reproduce the content of the NOTICE file. -# -# You may obtain a copy of the Apache License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the Apache License with the above modification is -# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -# KIND, either express or implied. See the Apache License for the specific -# language governing permissions and limitations under the Apache License. -# import unittest @@ -83,8 +62,18 @@ def test_str(self): ) ) + def test_setters(self): + ef = otio.schema.Effect( + name="blur it", + effect_name="blur", + metadata={"foo": "bar"} + ) + self.assertEqual(ef.effect_name, "blur") + ef.effect_name = "flop" + self.assertEqual(ef.effect_name, "flop") + -class TestLinearTimeWarp(unittest.TestCase): +class TestLinearTimeWarp(unittest.TestCase, otio_test_utils.OTIOAssertions): def test_cons(self): ef = otio.schema.LinearTimeWarp("Foo", 2.5, {'foo': 'bar'}) self.assertEqual(ef.effect_name, "LinearTimeWarp") @@ -92,6 +81,18 @@ def test_cons(self): self.assertEqual(ef.time_scalar, 2.5) self.assertEqual(ef.metadata, {"foo": "bar"}) + def test_serialize(self): + ef = otio.schema.LinearTimeWarp("Foo", 2.5, {'foo': 'bar'}) + encoded = otio.adapters.otio_json.write_to_string(ef) + decoded = otio.adapters.otio_json.read_from_string(encoded) + self.assertIsOTIOEquivalentTo(ef, decoded) + + def test_setters(self): + ef = otio.schema.LinearTimeWarp("Foo", 2.5, {'foo': 'bar'}) + self.assertEqual(ef.time_scalar, 2.5) + ef.time_scalar = 5.0 + self.assertEqual(ef.time_scalar, 5.0) + class TestFreezeFrame(unittest.TestCase): def test_cons(self): diff --git a/tests/test_examples.py b/tests/test_examples.py index 7cbf47bc3..fbac9dcff 100644 --- a/tests/test_examples.py +++ b/tests/test_examples.py @@ -1,26 +1,5 @@ -# +# SPDX-License-Identifier: Apache-2.0 # Copyright Contributors to the OpenTimelineIO project -# -# Licensed under the Apache License, Version 2.0 (the "Apache License") -# with the following modification; you may not use this file except in -# compliance with the Apache License and the following modification to it: -# Section 6. Trademarks. is deleted and replaced with: -# -# 6. Trademarks. This License does not grant permission to use the trade -# names, trademarks, service marks, or product names of the Licensor -# and its affiliates, except as required to comply with Section 4(c) of -# the License and to reproduce the content of the NOTICE file. -# -# You may obtain a copy of the Apache License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the Apache License with the above modification is -# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -# KIND, either express or implied. See the Apache License for the specific -# language governing permissions and limitations under the Apache License. -# """Unit tests for the 'examples'""" import unittest @@ -29,13 +8,7 @@ import subprocess -# handle python2 vs python3 difference -try: - from tempfile import TemporaryDirectory # noqa: F401 - import tempfile -except ImportError: - # XXX: python2.7 only - from backports import tempfile +import tempfile import opentimelineio as otio diff --git a/tests/test_fcp7_xml_adapter.py b/tests/test_fcp7_xml_adapter.py index 464a89d0f..b355ace78 100644 --- a/tests/test_fcp7_xml_adapter.py +++ b/tests/test_fcp7_xml_adapter.py @@ -1,26 +1,5 @@ -# +# SPDX-License-Identifier: Apache-2.0 # Copyright Contributors to the OpenTimelineIO project -# -# Licensed under the Apache License, Version 2.0 (the "Apache License") -# with the following modification; you may not use this file except in -# compliance with the Apache License and the following modification to it: -# Section 6. Trademarks. is deleted and replaced with: -# -# 6. Trademarks. This License does not grant permission to use the trade -# names, trademarks, service marks, or product names of the Licensor -# and its affiliates, except as required to comply with Section 4(c) of -# the License and to reproduce the content of the NOTICE file. -# -# You may obtain a copy of the Apache License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the Apache License with the above modification is -# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -# KIND, either express or implied. See the Apache License for the specific -# language governing permissions and limitations under the Apache License. -# """Test final cut pro xml.""" @@ -952,7 +931,7 @@ class AdaptersFcp7XmlTest(unittest.TestCase, test_utils.OTIOAssertions): adapter = adapters.from_name('fcp_xml').module() def __init__(self, *args, **kwargs): - super(AdaptersFcp7XmlTest, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) self.maxDiff = None def test_build_empty_file(self): @@ -1187,12 +1166,12 @@ def test_roundtrip_mem2disk2mem(self): }, ) - v0 = schema.Track(kind=schema.track.TrackKind.Video) - v1 = schema.Track(kind=schema.track.TrackKind.Video) + v0 = schema.Track(kind=schema.TrackKind.Video) + v1 = schema.Track(kind=schema.TrackKind.Video) timeline.tracks.extend([v0, v1]) - a0 = schema.Track(kind=schema.track.TrackKind.Audio) + a0 = schema.Track(kind=schema.TrackKind.Audio) timeline.tracks.append(a0) @@ -1277,6 +1256,17 @@ def test_roundtrip_mem2disk2mem(self): ) ) + timeline.tracks.markers.append( + schema.Marker( + name='test_timeline_marker_range', + marked_range=opentime.TimeRange( + opentime.RationalTime(123, RATE), + opentime.RationalTime(11, RATE), + ), + metadata={'fcp_xml': {'comment': 'my_comment'}} + ) + ) + v1[1].markers.append( schema.Marker( name='test_clip_marker', @@ -1287,6 +1277,17 @@ def test_roundtrip_mem2disk2mem(self): ) ) + v1[1].markers.append( + schema.Marker( + name='test_clip_marker_range', + marked_range=opentime.TimeRange( + opentime.RationalTime(125, RATE), + opentime.RationalTime(6, RATE) + ), + metadata={'fcp_xml': {'comment': 'my_comment'}} + ) + ) + # make sure that global_start_time.rate survives the round trip timeline.global_start_time = opentime.RationalTime(100, RATE) @@ -1308,10 +1309,10 @@ def test_roundtrip_mem2disk2mem(self): # Before comparing, scrub ignorable metadata introduced in # serialization (things like unique ids minted by the adapter) # Since we seeded metadata for the generator, keep that metadata - del(new_timeline.metadata["fcp_xml"]) - for child in new_timeline.tracks.each_child(): + del new_timeline.metadata["fcp_xml"] + for child in new_timeline.tracks.find_children(): try: - del(child.metadata["fcp_xml"]) + del child.metadata["fcp_xml"] except KeyError: pass @@ -1320,7 +1321,7 @@ def test_roundtrip_mem2disk2mem(self): child.media_reference, schema.GeneratorReference ) if not is_generator: - del(child.media_reference.metadata["fcp_xml"]) + del child.media_reference.metadata["fcp_xml"] except (AttributeError, KeyError): pass @@ -1344,7 +1345,7 @@ def scrub_md_dicts(timeline): def scrub_displayformat(md_dict): for ignore_key in {"link"}: try: - del(md_dict[ignore_key]) + del md_dict[ignore_key] except KeyError: pass @@ -1355,7 +1356,7 @@ def scrub_displayformat(md_dict): except AttributeError: pass - for child in timeline.tracks.each_child(): + for child in timeline.tracks.find_children(): scrub_displayformat(child.metadata) try: scrub_displayformat(child.media_reference.metadata) @@ -1376,8 +1377,8 @@ def scrub_displayformat(md_dict): # But the xml text on disk is not identical because otio has a subset # of features to xml and we drop all the nle specific preferences. - with open(FCP7_XML_EXAMPLE_PATH, "r") as original_file: - with open(tmp_path, "r") as output_file: + with open(FCP7_XML_EXAMPLE_PATH) as original_file: + with open(tmp_path) as output_file: self.assertNotEqual(original_file.read(), output_file.read()) def test_hiero_flavored_xml(self): @@ -1385,7 +1386,7 @@ def test_hiero_flavored_xml(self): self.assertTrue(len(timeline.tracks), 1) self.assertTrue(timeline.tracks[0].name == 'Video 1') - clips = [c for c in timeline.tracks[0].each_clip()] + clips = [c for c in timeline.tracks[0].find_clips()] self.assertTrue(len(clips), 2) self.assertTrue(clips[0].name == 'A160C005_171213_R0MN') @@ -1432,8 +1433,8 @@ def test_hiero_flavored_xml(self): # Similar to the test_roundtrip_disk2mem2disk above # the track name element among others will not be present in a new xml. - with open(HIERO_XML_PATH, "r") as original_file: - with open(tmp_path, "r") as output_file: + with open(HIERO_XML_PATH) as original_file: + with open(tmp_path) as output_file: self.assertNotEqual(original_file.read(), output_file.read()) def test_xml_with_empty_elements(self): diff --git a/tests/test_filter_algorithms.py b/tests/test_filter_algorithms.py index ec1832b47..4907bce32 100644 --- a/tests/test_filter_algorithms.py +++ b/tests/test_filter_algorithms.py @@ -1,27 +1,7 @@ #!/usr/bin/env python # +# SPDX-License-Identifier: Apache-2.0 # Copyright Contributors to the OpenTimelineIO project -# -# Licensed under the Apache License, Version 2.0 (the "Apache License") -# with the following modification; you may not use this file except in -# compliance with the Apache License and the following modification to it: -# Section 6. Trademarks. is deleted and replaced with: -# -# 6. Trademarks. This License does not grant permission to use the trade -# names, trademarks, service marks, or product names of the Licensor -# and its affiliates, except as required to comply with Section 4(c) of -# the License and to reproduce the content of the NOTICE file. -# -# You may obtain a copy of the Apache License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the Apache License with the above modification is -# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -# KIND, either express or implied. See the Apache License for the specific -# language governing permissions and limitations under the Apache License. -# """Test harness for the filter algorithms.""" diff --git a/tests/test_generator_reference.py b/tests/test_generator_reference.py index 77a3cb7be..aa98db7cd 100644 --- a/tests/test_generator_reference.py +++ b/tests/test_generator_reference.py @@ -1,3 +1,6 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright Contributors to the OpenTimelineIO project + """ Generator Reference class test harness. """ import unittest diff --git a/tests/test_hooks_plugins.py b/tests/test_hooks_plugins.py index c14ac0151..97d0367fc 100644 --- a/tests/test_hooks_plugins.py +++ b/tests/test_hooks_plugins.py @@ -1,26 +1,6 @@ -# +# SPDX-License-Identifier: Apache-2.0 # Copyright Contributors to the OpenTimelineIO project -# -# Licensed under the Apache License, Version 2.0 (the "Apache License") -# with the following modification; you may not use this file except in -# compliance with the Apache License and the following modification to it: -# Section 6. Trademarks. is deleted and replaced with: -# -# 6. Trademarks. This License does not grant permission to use the trade -# names, trademarks, service marks, or product names of the Licensor -# and its affiliates, except as required to comply with Section 4(c) of -# the License and to reproduce the content of the NOTICE file. -# -# You may obtain a copy of the Apache License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the Apache License with the above modification is -# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -# KIND, either express or implied. See the Apache License for the specific -# language governing permissions and limitations under the Apache License. -# + import unittest import os from copy import deepcopy @@ -34,13 +14,7 @@ utils, ) -# handle python2 vs python3 difference -try: - from tempfile import TemporaryDirectory # noqa: F401 - import tempfile -except ImportError: - # XXX: python2.7 only - from backports import tempfile +import tempfile HOOKSCRIPT_PATH = "hookscript_example" @@ -67,7 +41,6 @@ def setUp(self): def test_plugin_hook(self): self.assertEqual(self.hook_script.name, "example hook") - self.assertEqual(self.hook_script.execution_scope, "in process") self.assertEqual(self.hook_script.filepath, "example.py") def test_plugin_hook_runs(self): @@ -109,7 +82,6 @@ def tearDown(self): def test_plugin_adapter(self): self.assertEqual(self.hsf.name, "example hook") - self.assertEqual(self.hsf.execution_scope, "in process") self.assertEqual(self.hsf.filepath, "example.py") def test_load_adapter_module(self): @@ -169,9 +141,8 @@ def test_serialize(self): self.assertEqual( str(self.hsf), - "HookScript({}, {}, {})".format( + "HookScript({}, {})".format( repr(self.hsf.name), - repr(self.hsf.execution_scope), repr(self.hsf.filepath) ) ) @@ -179,11 +150,9 @@ def test_serialize(self): repr(self.hsf), "otio.hooks.HookScript(" "name={}, " - "execution_scope={}, " "filepath={}" ")".format( repr(self.hsf.name), - repr(self.hsf.execution_scope), repr(self.hsf.filepath) ) ) diff --git a/tests/test_image_sequence_reference.py b/tests/test_image_sequence_reference.py index 73e616179..1cf419eda 100644 --- a/tests/test_image_sequence_reference.py +++ b/tests/test_image_sequence_reference.py @@ -1,14 +1,13 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright Contributors to the OpenTimelineIO project + """Test harness for Image Sequence References.""" import unittest -import sys import opentimelineio as otio import opentimelineio.test_utils as otio_test_utils -IS_PYTHON_2 = (sys.version_info < (3, 0)) - - class ImageSequenceReferenceTests( unittest.TestCase, otio_test_utils.OTIOAssertions ): @@ -49,7 +48,6 @@ def test_create(self): otio.schema.ImageSequenceReference.MissingFramePolicy.hold, ) - @unittest.skipIf(IS_PYTHON_2, "unicode strings do funny things in python2") def test_str(self): ref = otio.schema.ImageSequenceReference( "file:///show/seq/shot/rndr/", @@ -86,7 +84,6 @@ def test_str(self): ')' ) - @unittest.skipIf(IS_PYTHON_2, "unicode strings do funny things in python2") def test_repr(self): ref = otio.schema.ImageSequenceReference( "file:///show/seq/shot/rndr/", @@ -238,7 +235,7 @@ def test_number_of_images_in_sequence_with_skip(self): def test_target_url_for_image_number(self): all_images_urls = [ - "file:///show/seq/shot/rndr/show_shot.{:04}.exr".format(i) + f"file:///show/seq/shot/rndr/show_shot.{i:04}.exr" for i in range(1, 49) ] ref = otio.schema.ImageSequenceReference( @@ -277,7 +274,7 @@ def test_target_url_for_image_number_steps(self): ) all_images_urls = [ - "file:///show/seq/shot/rndr/show_shot.{:04}.exr".format(i) + f"file:///show/seq/shot/rndr/show_shot.{i:04}.exr" for i in range(1, 49, 2) ] generated_urls = [ @@ -288,7 +285,7 @@ def test_target_url_for_image_number_steps(self): ref.frame_step = 3 all_images_urls_threes = [ - "file:///show/seq/shot/rndr/show_shot.{:04}.exr".format(i) + f"file:///show/seq/shot/rndr/show_shot.{i:04}.exr" for i in range(1, 49, 3) ] generated_urls_threes = [ @@ -300,7 +297,7 @@ def test_target_url_for_image_number_steps(self): ref.frame_step = 2 ref.start_frame = 0 all_images_urls_zero_first = [ - "file:///show/seq/shot/rndr/show_shot.{:04}.exr".format(i) + f"file:///show/seq/shot/rndr/show_shot.{i:04}.exr" for i in range(0, 48, 2) ] generated_urls_zero_first = [ @@ -621,7 +618,7 @@ def test_negative_frame_numbers(self): for i in range(1, ref.number_of_images_in_sequence()): self.assertEqual( ref.target_url_for_image_number(i), - "file:///show/seq/shot/rndr/show_shot.{:04}.exr".format(i - 1), + f"file:///show/seq/shot/rndr/show_shot.{i - 1:04}.exr", ) def test_target_url_for_image_number_with_missing_timing_info(self): @@ -683,7 +680,7 @@ def test_clone(self): cln = copy.deepcopy(isr) cln = isr.clone() except ValueError as exc: - self.fail("Cloning raised an exception: {}".format(exc)) + self.fail(f"Cloning raised an exception: {exc}") self.assertJsonEqual(isr, cln) diff --git a/tests/test_item.py b/tests/test_item.py index 57d1afeff..1df96245e 100755 --- a/tests/test_item.py +++ b/tests/test_item.py @@ -1,26 +1,5 @@ -# +# SPDX-License-Identifier: Apache-2.0 # Copyright Contributors to the OpenTimelineIO project -# -# Licensed under the Apache License, Version 2.0 (the "Apache License") -# with the following modification; you may not use this file except in -# compliance with the Apache License and the following modification to it: -# Section 6. Trademarks. is deleted and replaced with: -# -# 6. Trademarks. This License does not grant permission to use the trade -# names, trademarks, service marks, or product names of the Licensor -# and its affiliates, except as required to comply with Section 4(c) of -# the License and to reproduce the content of the NOTICE file. -# -# You may obtain a copy of the Apache License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the Apache License with the above modification is -# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -# KIND, either express or implied. See the Apache License for the specific -# language governing permissions and limitations under the Apache License. -# """Test harness for Item.""" @@ -256,6 +235,13 @@ def test_metadata(self): self.assertIsOTIOEquivalentTo(it, decoded) self.assertEqual(decoded.metadata["foo"], it.metadata["foo"]) + foo = it.metadata.pop("foo") + self.assertEqual(foo, "bar") + foo = it.metadata.pop("foo", "default") + self.assertEqual(foo, "default") + with self.assertRaises(KeyError): + it.metadata.pop("foo") + def test_add_effect(self): tr = otio.opentime.TimeRange( duration=otio.opentime.RationalTime(10, 1) @@ -451,7 +437,7 @@ def test_visible_range(self): self.maxDiff = None self.assertListEqual( ["A", "B", "C", "D"], - [item.name for item in timeline.each_clip()] + [item.name for item in timeline.find_clips()] ) self.assertListEqual( [ @@ -496,7 +482,7 @@ def test_visible_range(self): ) ), ], - [item.trimmed_range() for item in timeline.each_clip()] + [item.trimmed_range() for item in timeline.find_clips()] ) self.assertListEqual( @@ -542,7 +528,7 @@ def test_visible_range(self): ) ), ], - [item.visible_range() for item in timeline.each_clip()] + [item.visible_range() for item in timeline.find_clips()] ) diff --git a/tests/test_json_backend.py b/tests/test_json_backend.py index 81a5b27d6..f649f4b1c 100755 --- a/tests/test_json_backend.py +++ b/tests/test_json_backend.py @@ -1,26 +1,5 @@ -# +# SPDX-License-Identifier: Apache-2.0 # Copyright Contributors to the OpenTimelineIO project -# -# Licensed under the Apache License, Version 2.0 (the "Apache License") -# with the following modification; you may not use this file except in -# compliance with the Apache License and the following modification to it: -# Section 6. Trademarks. is deleted and replaced with: -# -# 6. Trademarks. This License does not grant permission to use the trade -# names, trademarks, service marks, or product names of the Licensor -# and its affiliates, except as required to comply with Section 4(c) of -# the License and to reproduce the content of the NOTICE file. -# -# You may obtain a copy of the Apache License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the Apache License with the above modification is -# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -# KIND, either express or implied. See the Apache License for the specific -# language governing permissions and limitations under the Apache License. -# """Unit tests for the JSON format OTIO Serializes to.""" diff --git a/tests/test_marker.py b/tests/test_marker.py index 3c251ff95..f05936b20 100755 --- a/tests/test_marker.py +++ b/tests/test_marker.py @@ -1,26 +1,5 @@ -# +# SPDX-License-Identifier: Apache-2.0 # Copyright Contributors to the OpenTimelineIO project -# -# Licensed under the Apache License, Version 2.0 (the "Apache License") -# with the following modification; you may not use this file except in -# compliance with the Apache License and the following modification to it: -# Section 6. Trademarks. is deleted and replaced with: -# -# 6. Trademarks. This License does not grant permission to use the trade -# names, trademarks, service marks, or product names of the Licensor -# and its affiliates, except as required to comply with Section 4(c) of -# the License and to reproduce the content of the NOTICE file. -# -# You may obtain a copy of the Apache License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the Apache License with the above modification is -# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -# KIND, either express or implied. See the Apache License for the specific -# language governing permissions and limitations under the Apache License. -# import unittest diff --git a/tests/test_media_linker.py b/tests/test_media_linker.py index 475fe0991..46d4195dc 100644 --- a/tests/test_media_linker.py +++ b/tests/test_media_linker.py @@ -1,26 +1,5 @@ -# +# SPDX-License-Identifier: Apache-2.0 # Copyright Contributors to the OpenTimelineIO project -# -# Licensed under the Apache License, Version 2.0 (the "Apache License") -# with the following modification; you may not use this file except in -# compliance with the Apache License and the following modification to it: -# Section 6. Trademarks. is deleted and replaced with: -# -# 6. Trademarks. This License does not grant permission to use the trade -# names, trademarks, service marks, or product names of the Licensor -# and its affiliates, except as required to comply with Section 4(c) of -# the License and to reproduce the content of the NOTICE file. -# -# You may obtain a copy of the Apache License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the Apache License with the above modification is -# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -# KIND, either express or implied. See the Apache License for the specific -# language governing permissions and limitations under the Apache License. -# import os import unittest @@ -52,7 +31,6 @@ def tearDown(self): def test_plugin_adapter(self): self.assertEqual(self.mln.name, "example") - self.assertEqual(self.mln.execution_scope, "in process") self.assertEqual(self.mln.filepath, "example.py") def test_load_adapter_module(self): @@ -78,9 +56,8 @@ def test_serialize(self): self.assertEqual( str(self.mln), - "MediaLinker({}, {}, {})".format( + "MediaLinker({}, {})".format( repr(self.mln.name), - repr(self.mln.execution_scope), repr(self.mln.filepath) ) ) @@ -88,11 +65,9 @@ def test_serialize(self): repr(self.mln), "otio.media_linker.MediaLinker(" "name={}, " - "execution_scope={}, " "filepath={}" ")".format( repr(self.mln.name), - repr(self.mln.execution_scope), repr(self.mln.filepath) ) ) diff --git a/tests/test_media_reference.py b/tests/test_media_reference.py index e72f82169..a810e4832 100755 --- a/tests/test_media_reference.py +++ b/tests/test_media_reference.py @@ -1,26 +1,5 @@ -# +# SPDX-License-Identifier: Apache-2.0 # Copyright Contributors to the OpenTimelineIO project -# -# Licensed under the Apache License, Version 2.0 (the "Apache License") -# with the following modification; you may not use this file except in -# compliance with the Apache License and the following modification to it: -# Section 6. Trademarks. is deleted and replaced with: -# -# 6. Trademarks. This License does not grant permission to use the trade -# names, trademarks, service marks, or product names of the Licensor -# and its affiliates, except as required to comply with Section 4(c) of -# the License and to reproduce the content of the NOTICE file. -# -# You may obtain a copy of the Apache License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the Apache License with the above modification is -# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -# KIND, either express or implied. See the Apache License for the specific -# language governing permissions and limitations under the Apache License. -# """Test harness for Media References.""" diff --git a/tests/test_multithreading.py b/tests/test_multithreading.py index 7d4c9b191..ec88c0e1f 100644 --- a/tests/test_multithreading.py +++ b/tests/test_multithreading.py @@ -1,26 +1,5 @@ -# +# SPDX-License-Identifier: Apache-2.0 # Copyright Contributors to the OpenTimelineIO project -# -# Licensed under the Apache License, Version 2.0 (the "Apache License") -# with the following modification; you may not use this file except in -# compliance with the Apache License and the following modification to it: -# Section 6. Trademarks. is deleted and replaced with: -# -# 6. Trademarks. This License does not grant permission to use the trade -# names, trademarks, service marks, or product names of the Licensor -# and its affiliates, except as required to comply with Section 4(c) of -# the License and to reproduce the content of the NOTICE file. -# -# You may obtain a copy of the Apache License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the Apache License with the above modification is -# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -# KIND, either express or implied. See the Apache License for the specific -# language governing permissions and limitations under the Apache License. -# import unittest import threading diff --git a/tests/test_opentime.cpp b/tests/test_opentime.cpp index 211140748..b508da899 100644 --- a/tests/test_opentime.cpp +++ b/tests/test_opentime.cpp @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Contributors to the OpenTimelineIO project + #include "utils.h" #include @@ -35,6 +38,75 @@ main(int argc, char** argv) assertFalse(t1 != t3); }); + tests.add_test("test_from_time_string", [] { + std::string time_string = "0:12:04"; + auto t = otime::RationalTime(24 * (12 * 60 + 4), 24); + auto time_obj = otime::RationalTime::from_time_string(time_string, 24); + assertTrue(t.almost_equal(time_obj, 0.001)); + }); + + tests.add_test("test_from_time_string24", [] { + std::string time_string = "00:00:00.041667"; + auto t = otime::RationalTime(1, 24); + auto time_obj = otime::RationalTime::from_time_string(time_string, 24); + assertTrue(t.almost_equal(time_obj, 0.001)); + time_string = "00:00:01"; + t = otime::RationalTime(24, 24); + time_obj = otime::RationalTime::from_time_string(time_string, 24); + assertTrue(t.almost_equal(time_obj, 0.001)); + time_string = "00:01:00"; + t = otime::RationalTime(60 * 24, 24); + time_obj = otime::RationalTime::from_time_string(time_string, 24); + assertTrue(t.almost_equal(time_obj, 0.001)); + time_string = "01:00:00"; + t = otime::RationalTime(60 * 60 * 24, 24); + time_obj = otime::RationalTime::from_time_string(time_string, 24); + assertTrue(t.almost_equal(time_obj, 0.001)); + time_string = "24:00:00"; + t = otime::RationalTime(24 * 60 * 60 * 24, 24); + time_obj = otime::RationalTime::from_time_string(time_string, 24); + assertTrue(t.almost_equal(time_obj, 0.001)); + time_string = "23:59:59.92"; + t = otime::RationalTime((23 * 60 * 60 + 59 * 60 + 59.92) * 24, 24); + time_obj = otime::RationalTime::from_time_string(time_string, 24); + assertTrue(t.almost_equal(time_obj, 0.001)); + }); + + tests.add_test("test_from_time_string25", [] { + std::string time_string = "0:12:04.929792"; + auto t = otime::RationalTime((12 * 60 + 4.929792) * 25, 25); + auto time_obj = otime::RationalTime::from_time_string(time_string, 24); + assertTrue(t.almost_equal(time_obj, 0.001)); + time_string = "00:00:01"; + t = otime::RationalTime(25, 25); + time_obj = otime::RationalTime::from_time_string(time_string, 24); + assertTrue(t.almost_equal(time_obj, 0.001)); + time_string = "0:1"; + t = otime::RationalTime(25, 25); + time_obj = otime::RationalTime::from_time_string(time_string, 24); + assertTrue(t.almost_equal(time_obj, 0.001)); + time_string = "1"; + t = otime::RationalTime(25, 25); + time_obj = otime::RationalTime::from_time_string(time_string, 24); + assertTrue(t.almost_equal(time_obj, 0.001)); + time_string = "00:01:00"; + t = otime::RationalTime(60 * 25, 25); + time_obj = otime::RationalTime::from_time_string(time_string, 24); + assertTrue(t.almost_equal(time_obj, 0.001)); + time_string = "01:00:00"; + t = otime::RationalTime(60 * 60 * 25, 25); + time_obj = otime::RationalTime::from_time_string(time_string, 24); + assertTrue(t.almost_equal(time_obj, 0.001)); + time_string = "24:00:00"; + t = otime::RationalTime(24 * 60 * 60 * 25, 25); + time_obj = otime::RationalTime::from_time_string(time_string, 24); + assertTrue(t.almost_equal(time_obj, 0.001)); + time_string = "23:59:59.92"; + t = otime::RationalTime((23 * 60 * 60 + 59 * 60 + 59.92) * 25, 25); + time_obj = otime::RationalTime::from_time_string(time_string, 24); + assertTrue(t.almost_equal(time_obj, 0.001)); + }); + tests.run(argc, argv); return 0; } diff --git a/tests/test_opentime.py b/tests/test_opentime.py index 6921ebd6f..5afc847b7 100755 --- a/tests/test_opentime.py +++ b/tests/test_opentime.py @@ -1,26 +1,5 @@ -# +# SPDX-License-Identifier: Apache-2.0 # Copyright Contributors to the OpenTimelineIO project -# -# Licensed under the Apache License, Version 2.0 (the "Apache License") -# with the following modification; you may not use this file except in -# compliance with the Apache License and the following modification to it: -# Section 6. Trademarks. is deleted and replaced with: -# -# 6. Trademarks. This License does not grant permission to use the trade -# names, trademarks, service marks, or product names of the Licensor -# and its affiliates, except as required to comply with Section 4(c) of -# the License and to reproduce the content of the NOTICE file. -# -# You may obtain a copy of the Apache License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the Apache License with the above modification is -# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -# KIND, either express or implied. See the Apache License for the specific -# language governing permissions and limitations under the Apache License. -# """Test Harness for the otio.opentime library.""" @@ -38,6 +17,11 @@ def test_create(self): self.assertIsNotNone(t) self.assertEqual(t.value, t_val) + t_val = -30.2 + t = otio.opentime.RationalTime(t_val) + self.assertIsNotNone(t) + self.assertEqual(t.value, t_val) + t = otio.opentime.RationalTime() self.assertEqual(t.value, 0) self.assertEqual(t.rate, 1.0) @@ -94,7 +78,6 @@ def test_deepcopy(self): self.assertEqual(t2, otio.opentime.RationalTime(18, 24)) def test_base_conversion(self): - # from a number t = otio.opentime.RationalTime(10, 24) with self.assertRaises(TypeError): @@ -114,6 +97,14 @@ def test_time_timecode_convert(self): t = otio.opentime.from_timecode(timecode, 24) self.assertEqual(timecode, otio.opentime.to_timecode(t)) + def test_negative_timecode(self): + with self.assertRaises(ValueError): + otio.opentime.from_timecode('-01:00:13:13', 24) + + def test_bogus_timecode(self): + with self.assertRaises(ValueError): + otio.opentime.from_timecode('pink elephants', 13) + def test_time_timecode_convert_bad_rate(self): with self.assertRaises(ValueError) as exception_manager: otio.opentime.from_timecode('01:00:13:24', 24) @@ -339,6 +330,20 @@ def test_timecode_ntsc_2997fps(self): invalid_df_rate, (24000 / 1001.0), drop_frame=True ) + def test_timecode_infer_drop_frame(self): + frames = 1084319 + rates = [ + (29.97, '10:03:00;05'), + (30000.0 / 1001.0, '10:03:00;05'), + (59.94, '05:01:30;03'), + (60000.0 / 1001.0, '05:01:11;59') + ] + for rate, timecode in rates: + t = otio.opentime.RationalTime(frames, rate) + + self.assertEqual(t.to_timecode(rate, drop_frame=None), timecode) + self.assertEqual(t.to_timecode(rate), timecode) + def test_timecode_2997(self): ref_values = [ (10789, '00:05:59:19', '00:05:59;29'), diff --git a/tests/test_otiod.py b/tests/test_otiod.py index debde0768..f62253c91 100644 --- a/tests/test_otiod.py +++ b/tests/test_otiod.py @@ -1,27 +1,7 @@ #!/usr/bin/env python # +# SPDX-License-Identifier: Apache-2.0 # Copyright Contributors to the OpenTimelineIO project -# -# Licensed under the Apache License, Version 2.0 (the "Apache License") -# with the following modification; you may not use this file except in -# compliance with the Apache License and the following modification to it: -# Section 6. Trademarks. is deleted and replaced with: -# -# 6. Trademarks. This License does not grant permission to use the trade -# names, trademarks, service marks, or product names of the Licensor -# and its affiliates, except as required to comply with Section 4(c) of -# the License and to reproduce the content of the NOTICE file. -# -# You may obtain a copy of the Apache License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the Apache License with the above modification is -# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -# KIND, either express or implied. See the Apache License for the specific -# language governing permissions and limitations under the Apache License. -# """Tests for the OTIOD adapter.""" @@ -62,7 +42,7 @@ def setUp(self): # convert to contrived local reference last_rel = False - for cl in tl.each_clip(): + for cl in tl.find_clips(): # vary the relative and absolute paths, make sure that both work next_rel = ( MEDIA_EXAMPLE_PATH_URL_REL if last_rel else MEDIA_EXAMPLE_PATH_URL_ABS @@ -85,7 +65,7 @@ def test_file_bundle_manifest_missing_reference(self): ) self.assertEqual(manifest, {}) - for cl in result_otio.each_clip(): + for cl in result_otio.find_clips(): self.assertIsInstance( cl.media_reference, otio.schema.MissingReference, @@ -122,7 +102,8 @@ def test_file_bundle_manifest(self): self.assertEqual(len(manifest[fname]), count) def test_round_trip(self): - tmp_path = tempfile.NamedTemporaryFile(suffix=".otiod").name + with tempfile.NamedTemporaryFile(suffix=".otiod") as bogusfile: + tmp_path = bogusfile.name otio.adapters.write_to_file(self.tl, tmp_path) self.assertTrue(os.path.exists(tmp_path)) @@ -131,14 +112,14 @@ def test_round_trip(self): tmp_path, ) - for cl in result.each_clip(): + for cl in result.find_clips(): self.assertNotEqual( cl.media_reference.target_url, MEDIA_EXAMPLE_PATH_URL_REL ) # conform media references in input to what they should be in the output - for cl in self.tl.each_clip(): + for cl in self.tl.find_clips(): # construct an absolute file path to the result cl.media_reference.target_url = ( otio.url_utils.url_from_filepath( @@ -152,7 +133,8 @@ def test_round_trip(self): self.assertJsonEqual(result, self.tl) def test_round_trip_all_missing_references(self): - tmp_path = tempfile.NamedTemporaryFile(suffix=".otiod").name + with tempfile.NamedTemporaryFile(suffix=".otiod") as bogusfile: + tmp_path = bogusfile.name otio.adapters.write_to_file( self.tl, tmp_path, @@ -167,14 +149,15 @@ def test_round_trip_all_missing_references(self): absolute_media_reference_paths=True ) - for cl in result.each_clip(): + for cl in result.find_clips(): self.assertIsInstance( cl.media_reference, otio.schema.MissingReference ) def test_round_trip_absolute_paths(self): - tmp_path = tempfile.NamedTemporaryFile(suffix=".otiod").name + with tempfile.NamedTemporaryFile(suffix=".otiod") as bogusfile: + tmp_path = bogusfile.name otio.adapters.write_to_file(self.tl, tmp_path) # ...but can be optionally told to generate absolute paths @@ -183,14 +166,14 @@ def test_round_trip_absolute_paths(self): absolute_media_reference_paths=True ) - for cl in result.each_clip(): + for cl in result.find_clips(): self.assertNotEqual( cl.media_reference.target_url, MEDIA_EXAMPLE_PATH_URL_REL ) # conform media references in input to what they should be in the output - for cl in self.tl.each_clip(): + for cl in self.tl.find_clips(): # should be only field that changed cl.media_reference.target_url = ( otio.url_utils.url_from_filepath( diff --git a/tests/test_otioz.py b/tests/test_otioz.py index 3efd7ba19..09ed409e8 100644 --- a/tests/test_otioz.py +++ b/tests/test_otioz.py @@ -1,27 +1,7 @@ #!/usr/bin/env python # +# SPDX-License-Identifier: Apache-2.0 # Copyright Contributors to the OpenTimelineIO project -# -# Licensed under the Apache License, Version 2.0 (the "Apache License") -# with the following modification; you may not use this file except in -# compliance with the Apache License and the following modification to it: -# Section 6. Trademarks. is deleted and replaced with: -# -# 6. Trademarks. This License does not grant permission to use the trade -# names, trademarks, service marks, or product names of the Licensor -# and its affiliates, except as required to comply with Section 4(c) of -# the License and to reproduce the content of the NOTICE file. -# -# You may obtain a copy of the Apache License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the Apache License with the above modification is -# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -# KIND, either express or implied. See the Apache License for the specific -# language governing permissions and limitations under the Apache License. -# """Tests for the OTIOZ adapter.""" @@ -30,12 +10,7 @@ import tempfile import shutil -try: - # Python 2.7 - import urlparse -except ImportError: - # Python 3 - import urllib.parse as urlparse +import urllib.parse as urlparse import opentimelineio as otio import opentimelineio.test_utils as otio_test_utils @@ -68,7 +43,7 @@ def setUp(self): # convert to contrived local reference last_rel = False - for cl in tl.each_clip(): + for cl in tl.find_clips(): # vary the relative and absolute paths, make sure that both work next_rel = ( MEDIA_EXAMPLE_PATH_URL_REL @@ -99,15 +74,15 @@ def test_not_a_file_error(self): tmp_path = tempfile.mkstemp(suffix=".otioz", text=False)[1] with tempfile.NamedTemporaryFile() as bogusfile: fname = bogusfile.name - for cl in self.tl.each_clip(): + for cl in self.tl.find_clips(): # write with a non-file schema cl.media_reference = otio.schema.ExternalReference( - target_url="http://{}".format(fname) + target_url=f"http://{fname}" ) with self.assertRaises(otio.exceptions.OTIOError): otio.adapters.write_to_file(self.tl, tmp_path, dryrun=True) - for cl in self.tl.each_clip(): + for cl in self.tl.find_clips(): cl.media_reference = otio.schema.ExternalReference( target_url=otio.url_utils.url_from_filepath(fname) ) @@ -117,7 +92,7 @@ def test_not_a_file_error(self): tempdir = tempfile.mkdtemp() fname = tempdir shutil.rmtree(tempdir) - for cl in self.tl.each_clip(): + for cl in self.tl.find_clips(): cl.media_reference = otio.schema.ExternalReference(target_url=fname) def test_colliding_basename(self): @@ -130,7 +105,7 @@ def test_colliding_basename(self): MEDIA_EXAMPLE_PATH_ABS, new_path ) - list(self.tl.each_clip())[0].media_reference.target_url = ( + list(self.tl.find_clips())[0].media_reference.target_url = ( otio.url_utils.url_from_filepath(new_path) ) @@ -144,13 +119,14 @@ def test_colliding_basename(self): shutil.rmtree(tempdir) def test_round_trip(self): - tmp_path = tempfile.NamedTemporaryFile(suffix=".otioz").name + with tempfile.NamedTemporaryFile(suffix=".otioz") as bogusfile: + tmp_path = bogusfile.name otio.adapters.write_to_file(self.tl, tmp_path) self.assertTrue(os.path.exists(tmp_path)) result = otio.adapters.read_from_file(tmp_path) - for cl in result.each_clip(): + for cl in result.find_clips(): self.assertNotIn( cl.media_reference.target_url, [MEDIA_EXAMPLE_PATH_URL_ABS, MEDIA_EXAMPLE_PATH_URL_REL] @@ -166,7 +142,7 @@ def test_round_trip(self): ) # conform media references in input to what they should be in the output - for cl in self.tl.each_clip(): + for cl in self.tl.find_clips(): # should be only field that changed cl.media_reference.target_url = "media/{}".format( os.path.basename(cl.media_reference.target_url) @@ -175,7 +151,8 @@ def test_round_trip(self): self.assertJsonEqual(result, self.tl) def test_round_trip_with_extraction(self): - tmp_path = tempfile.NamedTemporaryFile(suffix=".otioz").name + with tempfile.NamedTemporaryFile(suffix=".otioz") as bogusfile: + tmp_path = bogusfile.name otio.adapters.write_to_file(self.tl, tmp_path) self.assertTrue(os.path.exists(tmp_path)) @@ -186,14 +163,14 @@ def test_round_trip_with_extraction(self): ) # make sure that all the references are ExternalReference - for cl in result.each_clip(): + for cl in result.find_clips(): self.assertIsInstance( cl.media_reference, otio.schema.ExternalReference ) # conform media references in input to what they should be in the output - for cl in self.tl.each_clip(): + for cl in self.tl.find_clips(): # should be only field that changed cl.media_reference.target_url = "media/{}".format( os.path.basename(cl.media_reference.target_url) @@ -233,7 +210,8 @@ def test_round_trip_with_extraction(self): ) def test_round_trip_with_extraction_no_media(self): - tmp_path = tempfile.NamedTemporaryFile(suffix=".otioz").name + with tempfile.NamedTemporaryFile(suffix=".otioz") as bogusfile: + tmp_path = bogusfile.name otio.adapters.write_to_file( self.tl, tmp_path, @@ -253,14 +231,14 @@ def test_round_trip_with_extraction_no_media(self): otio.adapters.file_bundle_utils.BUNDLE_VERSION_FILE ) self.assertTrue(os.path.exists(version_file_path)) - with open(version_file_path, 'r') as fi: + with open(version_file_path) as fi: self.assertEqual( fi.read(), otio.adapters.file_bundle_utils.BUNDLE_VERSION ) # conform media references in input to what they should be in the output - for cl in result.each_clip(): + for cl in result.find_clips(): # should be all MissingReferences self.assertIsInstance( cl.media_reference, diff --git a/tests/test_plugin_detection.py b/tests/test_plugin_detection.py index 217029e47..e7e4688a4 100644 --- a/tests/test_plugin_detection.py +++ b/tests/test_plugin_detection.py @@ -1,93 +1,61 @@ #!/usr/bin/env python # +# SPDX-License-Identifier: Apache-2.0 # Copyright Contributors to the OpenTimelineIO project -# -# Licensed under the Apache License, Version 2.0 (the "Apache License") -# with the following modification; you may not use this file except in -# compliance with the Apache License and the following modification to it: -# Section 6. Trademarks. is deleted and replaced with: -# -# 6. Trademarks. This License does not grant permission to use the trade -# names, trademarks, service marks, or product names of the Licensor -# and its affiliates, except as required to comply with Section 4(c) of -# the License and to reproduce the content of the NOTICE file. -# -# You may obtain a copy of the Apache License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the Apache License with the above modification is -# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -# KIND, either express or implied. See the Apache License for the specific -# language governing permissions and limitations under the Apache License. -# + import unittest import os -import pkg_resources +from pathlib import Path import sys -try: - # Python 3.3 forward includes the mock module - from unittest import mock - could_import_mock = True -except ImportError: - # Fallback for older python (not included in standard library) - try: - import mock - could_import_mock = True - except ImportError: - # Mock appears to not be installed - could_import_mock = False +from unittest import mock + +from importlib import reload as import_reload try: - # Python3: use importlib.reload - from importlib import reload as import_reload + import importlib.metadata as metadata except ImportError: - # Python2: - from imp import reload as import_reload + # For python 3.7 + import importlib_metadata as metadata import opentimelineio as otio from tests import baseline_reader -@unittest.skipIf( - not could_import_mock, - "mock module not found. Install mock from pypi or use python >= 3.3." -) class TestSetuptoolsPlugin(unittest.TestCase): def setUp(self): # Get the location of the mock plugin module metadata mock_module_path = os.path.join( - baseline_reader.path_to_baseline_directory(), + os.path.normpath(baseline_reader.path_to_baseline_directory()), 'plugin_module', ) - self.mock_module_manifest_path = os.path.join( + self.mock_module_manifest_path = Path( mock_module_path, "otio_jsonplugin", "plugin_manifest.json" - ) + ).absolute().as_posix() + + self.override_adapter_manifest_path = Path( + mock_module_path, + "otio_override_adapter", + "plugin_manifest.json" + ).absolute().as_posix() # Create a WorkingSet as if the module were installed - entries = [mock_module_path] + pkg_resources.working_set.entries + entries = [mock_module_path] + sys.path + + self.original_sysmodule_keys = set(sys.modules.keys()) self.sys_patch = mock.patch('sys.path', entries) self.sys_patch.start() - working_set = pkg_resources.WorkingSet(entries) - - # linker from the entry point - self.entry_patcher = mock.patch( - 'pkg_resources.iter_entry_points', - working_set.iter_entry_points - ) - self.entry_patcher.start() - def tearDown(self): self.sys_patch.stop() - self.entry_patcher.stop() - if 'otio_mockplugin' in sys.modules: - del(sys.modules['otio_mockplugin']) + + # Remove any modules added under test. We cannot replace sys.modules with + # a copy from setUp. For more, see: https://bugs.python.org/msg188914 + for key in set(sys.modules.keys()) ^ self.original_sysmodule_keys: + sys.modules.pop(key) def test_detect_plugin(self): """This manifest uses the plugin_manifest function""" @@ -110,8 +78,31 @@ def test_detect_plugin(self): for linker in man.media_linkers: self.assertIsInstance(linker, otio.media_linker.MediaLinker) - def test_pkg_resources_disabled(self): - os.environ["OTIO_DISABLE_PKG_RESOURCE_PLUGINS"] = "1" + def test_override_adapter(self): + + # Test that entrypoint plugins load before builtin and contrib + man = otio.plugins.manifest.load_manifest() + + # The override_adapter creates another cmx_3600 adapter + adapters = [adapter for adapter in man.adapters + if adapter.name == "cmx_3600"] + + # More then one cmx_3600 adapter should exist. + self.assertTrue(len(adapters) > 1) + + # Override adapter should be the first adapter found + manifest = adapters[0].plugin_info_map().get('from manifest', None) + self.assertEqual(manifest, self.override_adapter_manifest_path) + + self.assertTrue( + any( + True for p in man.source_files + if self.override_adapter_manifest_path in p + ) + ) + + def test_entrypoints_disabled(self): + os.environ["OTIO_DISABLE_ENTRYPOINTS_PLUGINS"] = "1" import_reload(otio.plugins.manifest) # detection of the environment variable happens on import, force a @@ -119,9 +110,13 @@ def test_pkg_resources_disabled(self): with self.assertRaises(AssertionError): self.test_detect_plugin() + # override adapter should not be loaded either + with self.assertRaises(AssertionError): + self.test_override_adapter() + # remove the environment variable and reload again for usage in the # other tests - del os.environ["OTIO_DISABLE_PKG_RESOURCE_PLUGINS"] + del os.environ["OTIO_DISABLE_ENTRYPOINTS_PLUGINS"] import_reload(otio.plugins.manifest) def test_detect_plugin_json_manifest(self): @@ -189,6 +184,42 @@ def test_deduplicate_env_variable_paths(self): else: del os.environ['OTIO_PLUGIN_MANIFEST_PATH'] + def test_plugin_load_failure(self): + """When a plugin fails to load, ensure the exception message + is logged (and no exception thrown) + """ + + sys.modules['otio_mock_bad_module'] = mock.Mock( + name='otio_mock_bad_module', + plugin_manifest=mock.Mock( + side_effect=Exception("Mock Exception") + ) + ) + + entry_points = mock.patch( + 'opentimelineio.plugins.manifest.metadata.entry_points', + return_value=[ + metadata.EntryPoint( + 'mock_bad_module', + 'otio_mock_bad_module', + 'opentimelineio.plugins' + ) + ] + ) + + with self.assertLogs() as cm, entry_points: + # Load the above mock entrypoint, expect it to fail and log + otio.plugins.manifest.load_manifest() + + load_errors = [ + r for r in cm.records + if r.message.startswith( + "could not load plugin: mock_bad_module. " + "Exception is: Mock Exception" + ) + ] + self.assertEqual(len(load_errors), 1) + if __name__ == '__main__': unittest.main() diff --git a/tests/test_schemadef_plugin.py b/tests/test_schemadef_plugin.py index 71b5346cf..3ed7194e3 100755 --- a/tests/test_schemadef_plugin.py +++ b/tests/test_schemadef_plugin.py @@ -1,26 +1,6 @@ -# +# SPDX-License-Identifier: Apache-2.0 # Copyright Contributors to the OpenTimelineIO project -# -# Licensed under the Apache License, Version 2.0 (the "Apache License") -# with the following modification; you may not use this file except in -# compliance with the Apache License and the following modification to it: -# Section 6. Trademarks. is deleted and replaced with: -# -# 6. Trademarks. This License does not grant permission to use the trade -# names, trademarks, service marks, or product names of the Licensor -# and its affiliates, except as required to comply with Section 4(c) of -# the License and to reproduce the content of the NOTICE file. -# -# You may obtain a copy of the Apache License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the Apache License with the above modification is -# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -# KIND, either express or implied. See the Apache License for the specific -# language governing permissions and limitations under the Apache License. -# + import unittest import os diff --git a/tests/test_serializableCollection.cpp b/tests/test_serializableCollection.cpp new file mode 100644 index 000000000..1956b28e2 --- /dev/null +++ b/tests/test_serializableCollection.cpp @@ -0,0 +1,93 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Contributors to the OpenTimelineIO project + +#include "utils.h" + +#include +#include +#include +#include + +#include + +namespace otime = opentime::OPENTIME_VERSION; +namespace otio = opentimelineio::OPENTIMELINEIO_VERSION; + +int +main(int argc, char** argv) +{ + Tests tests; + + tests.add_test( + "test_find_children", [] { + using namespace otio; + otio::SerializableObject::Retainer cl = + new otio::Clip(); + otio::SerializableObject::Retainer tr = + new otio::Track(); + tr->append_child(cl); + otio::SerializableObject::Retainer tl = + new otio::Timeline(); + tl->tracks()->append_child(tr); + otio::SerializableObject::Retainer + sc = new otio::SerializableCollection(); + sc->insert_child(0, tl); + opentimelineio::v1_0::ErrorStatus err; + auto result = sc->find_children(&err, {}, false); + assertEqual(result.size(), 1); + assertEqual(result[0].value, cl.value); + }); + tests.add_test( + "test_find_children_search_range", [] { + using namespace otio; + const TimeRange range(RationalTime(0.0, 24.0), RationalTime(24.0, 24.0)); + otio::SerializableObject::Retainer cl0 = + new otio::Clip(); + cl0->set_source_range(range); + otio::SerializableObject::Retainer cl1 = + new otio::Clip(); + cl1->set_source_range(range); + otio::SerializableObject::Retainer cl2 = + new otio::Clip(); + cl2->set_source_range(range); + otio::SerializableObject::Retainer tr = + new otio::Track(); + tr->append_child(cl0); + tr->append_child(cl1); + tr->append_child(cl2); + otio::SerializableObject::Retainer tl = + new otio::Timeline(); + tl->tracks()->append_child(tr); + otio::SerializableObject::Retainer + sc = new otio::SerializableCollection(); + sc->insert_child(0, tl); + opentimelineio::v1_0::ErrorStatus err; + auto result = sc->find_children(&err, range); + assertEqual(result.size(), 1); + assertEqual(result[0].value, cl0.value); + }); + tests.add_test( + "test_find_children_shallow_search", [] { + using namespace otio; + otio::SerializableObject::Retainer cl = + new otio::Clip(); + otio::SerializableObject::Retainer tr = + new otio::Track(); + tr->append_child(cl); + otio::SerializableObject::Retainer tl = + new otio::Timeline(); + tl->tracks()->append_child(tr); + otio::SerializableObject::Retainer + sc = new otio::SerializableCollection(); + sc->insert_child(0, tl); + opentimelineio::v1_0::ErrorStatus err; + auto result = sc->find_children(&err, nullopt, true); + assertEqual(result.size(), 0); + result = sc->find_children(&err, nullopt, false); + assertEqual(result.size(), 1); + assertEqual(result[0].value, cl.value); + }); + + tests.run(argc, argv); + return 0; +} diff --git a/tests/test_serializable_collection.py b/tests/test_serializable_collection.py index 410b9affb..4be752d30 100644 --- a/tests/test_serializable_collection.py +++ b/tests/test_serializable_collection.py @@ -1,26 +1,5 @@ -# +# SPDX-License-Identifier: Apache-2.0 # Copyright Contributors to the OpenTimelineIO project -# -# Licensed under the Apache License, Version 2.0 (the "Apache License") -# with the following modification; you may not use this file except in -# compliance with the Apache License and the following modification to it: -# Section 6. Trademarks. is deleted and replaced with: -# -# 6. Trademarks. This License does not grant permission to use the trade -# names, trademarks, service marks, or product names of the Licensor -# and its affiliates, except as required to comply with Section 4(c) of -# the License and to reproduce the content of the NOTICE file. -# -# You may obtain a copy of the Apache License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the Apache License with the above modification is -# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -# KIND, either express or implied. See the Apache License for the specific -# language governing permissions and limitations under the Apache License. -# import unittest @@ -58,7 +37,7 @@ def test_iterable(self): children=[self.sc] ) - self.assertEqual(len(list(sc.each_clip())), 1) + self.assertEqual(len(list(sc.find_clips())), 1) # test deleting an item tmp = self.sc[0] @@ -67,6 +46,9 @@ def test_iterable(self): self.sc[0] = tmp self.assertEqual(self.sc[0], tmp) + with self.assertRaises(IndexError): + self.sc[100] + def test_serialize(self): encoded = otio.adapters.otio_json.write_to_string(self.sc) decoded = otio.adapters.otio_json.read_from_string(encoded) @@ -92,6 +74,54 @@ def test_repr(self): ")" ) + def test_find_children(self): + cl = otio.schema.Clip() + tr = otio.schema.Track() + tr.append(cl) + tl = otio.schema.Timeline() + tl.tracks.append(tr) + sc = otio.schema.SerializableCollection() + sc.append(tl) + result = sc.find_children(otio.schema.Clip) + self.assertEqual(len(result), 1) + self.assertEqual(result[0], cl) + + def test_find_children_search_range(self): + range = otio.opentime.TimeRange( + otio.opentime.RationalTime(0.0, 24.0), + otio.opentime.RationalTime(24.0, 24.0)) + cl0 = otio.schema.Clip() + cl0.source_range = range + cl1 = otio.schema.Clip() + cl1.source_range = range + cl2 = otio.schema.Clip() + cl2.source_range = range + tr = otio.schema.Track() + tr.append(cl0) + tr.append(cl1) + tr.append(cl2) + tl = otio.schema.Timeline() + tl.tracks.append(tr) + sc = otio.schema.SerializableCollection() + sc.append(tl) + result = sc.find_children(otio.schema.Clip, range) + self.assertEqual(len(result), 1) + self.assertEqual(result[0], cl0) + + def test_find_children_shallow_search(self): + cl = otio.schema.Clip() + tr = otio.schema.Track() + tr.append(cl) + tl = otio.schema.Timeline() + tl.tracks.append(tr) + sc = otio.schema.SerializableCollection() + sc.append(tl) + result = sc.find_children(otio.schema.Clip, shallow_search=True) + self.assertEqual(len(result), 0) + result = sc.find_children(otio.schema.Clip, shallow_search=False) + self.assertEqual(len(result), 1) + self.assertEqual(result[0], cl) + if __name__ == '__main__': unittest.main() diff --git a/tests/test_serializable_object.py b/tests/test_serializable_object.py index 6897ea5fc..788a3f798 100755 --- a/tests/test_serializable_object.py +++ b/tests/test_serializable_object.py @@ -1,32 +1,13 @@ #!/usr/bin/env python # +# SPDX-License-Identifier: Apache-2.0 # Copyright Contributors to the OpenTimelineIO project -# -# Licensed under the Apache License, Version 2.0 (the "Apache License") -# with the following modification; you may not use this file except in -# compliance with the Apache License and the following modification to it: -# Section 6. Trademarks. is deleted and replaced with: -# -# 6. Trademarks. This License does not grant permission to use the trade -# names, trademarks, service marks, or product names of the Licensor -# and its affiliates, except as required to comply with Section 4(c) of -# the License and to reproduce the content of the NOTICE file. -# -# You may obtain a copy of the Apache License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the Apache License with the above modification is -# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -# KIND, either express or implied. See the Apache License for the specific -# language governing permissions and limitations under the Apache License. -# import opentimelineio as otio import opentimelineio.test_utils as otio_test_utils import unittest +import json class OpenTimeTypeSerializerTest(unittest.TestCase): @@ -76,6 +57,7 @@ def test_copy_lib(self): # deep copy so_cp = copy.deepcopy(so) + self.assertIsNotNone(so_cp) self.assertIsOTIOEquivalentTo(so, so_cp) so_cp.metadata["foo"] = "bar" @@ -98,48 +80,6 @@ class Foo(otio.core.SerializableObjectWithMetadata): self.assertEqual(Foo, type(foo_copy)) - def test_schema_versioning(self): - @otio.core.register_type - class FakeThing(otio.core.SerializableObject): - _serializable_label = "Stuff.1" - foo_two = otio.core.serializable_field("foo_2", doc="test") - ft = FakeThing() - - self.assertEqual(ft.schema_name(), "Stuff") - self.assertEqual(ft.schema_version(), 1) - - with self.assertRaises(otio.exceptions.UnsupportedSchemaError): - otio.core.instance_from_schema( - "Stuff", - 2, - {"foo": "bar"} - ) - - ft = otio.core.instance_from_schema("Stuff", 1, {"foo": "bar"}) - self.assertEqual(ft._dynamic_fields['foo'], "bar") - - @otio.core.register_type - class FakeThing(otio.core.SerializableObject): - _serializable_label = "NewStuff.4" - foo_two = otio.core.serializable_field("foo_2") - - @otio.core.upgrade_function_for(FakeThing, 2) - def upgrade_one_to_two(_data_dict): - return {"foo_2": _data_dict["foo"]} - - @otio.core.upgrade_function_for(FakeThing, 3) - def upgrade_one_to_two_three(_data_dict): - return {"foo_3": _data_dict["foo_2"]} - - ft = otio.core.instance_from_schema("NewStuff", 1, {"foo": "bar"}) - self.assertEqual(ft._dynamic_fields['foo_3'], "bar") - - ft = otio.core.instance_from_schema("NewStuff", 3, {"foo_2": "bar"}) - self.assertEqual(ft._dynamic_fields['foo_3'], "bar") - - ft = otio.core.instance_from_schema("NewStuff", 4, {"foo_3": "bar"}) - self.assertEqual(ft._dynamic_fields['foo_3'], "bar") - def test_equality(self): o1 = otio.core.SerializableObject() o2 = otio.core.SerializableObject() @@ -149,12 +89,12 @@ def test_equality(self): def test_equivalence_symmetry(self): def test_equivalence(A, B, msg): - self.assertTrue(A.is_equivalent_to(B), "{}: A ~= B".format(msg)) - self.assertTrue(B.is_equivalent_to(A), "{}: B ~= A".format(msg)) + self.assertTrue(A.is_equivalent_to(B), f"{msg}: A ~= B") + self.assertTrue(B.is_equivalent_to(A), f"{msg}: B ~= A") def test_difference(A, B, msg): - self.assertFalse(A.is_equivalent_to(B), "{}: A ~= B".format(msg)) - self.assertFalse(B.is_equivalent_to(A), "{}: B ~= A".format(msg)) + self.assertFalse(A.is_equivalent_to(B), f"{msg}: A ~= B") + self.assertFalse(B.is_equivalent_to(A), f"{msg}: B ~= A") A = otio.core.Composable() B = otio.core.Composable() @@ -197,5 +137,128 @@ def test_cycle_detection(self): o.clone() +class VersioningTests(unittest.TestCase, otio_test_utils.OTIOAssertions): + def test_schema_definition(self): + """define a schema and instantiate it from python""" + + # ensure that the type hasn't already been registered + self.assertNotIn("Stuff", otio.core.type_version_map()) + + @otio.core.register_type + class FakeThing(otio.core.SerializableObject): + _serializable_label = "Stuff.1" + foo_two = otio.core.serializable_field("foo_2", doc="test") + ft = FakeThing() + + self.assertEqual(ft.schema_name(), "Stuff") + self.assertEqual(ft.schema_version(), 1) + + with self.assertRaises(otio.exceptions.UnsupportedSchemaError): + otio.core.instance_from_schema( + "Stuff", + 2, + {"foo": "bar"} + ) + + version_map = otio.core.type_version_map() + self.assertEqual(version_map["Stuff"], 1) + + ft = otio.core.instance_from_schema("Stuff", 1, {"foo": "bar"}) + self.assertEqual(ft._dynamic_fields['foo'], "bar") + + @unittest.skip("@TODO: disabled pending discussion") + def test_double_register_schema(self): + @otio.core.register_type + class DoubleReg(otio.core.SerializableObject): + _serializable_label = "Stuff.1" + foo_two = otio.core.serializable_field("foo_2", doc="test") + _ = DoubleReg() # quiet pyflakes + + # not allowed to register a type twice + with self.assertRaises(ValueError): + @otio.core.register_type + class DoubleReg(otio.core.SerializableObject): + _serializable_label = "Stuff.1" + + def test_upgrade_versions(self): + """Test adding an upgrade functions for a type""" + + @otio.core.register_type + class FakeThing(otio.core.SerializableObject): + _serializable_label = "NewStuff.4" + foo_two = otio.core.serializable_field("foo_2") + + @otio.core.upgrade_function_for(FakeThing, 2) + def upgrade_one_to_two(_data_dict): + return {"foo_2": _data_dict["foo"]} + + @otio.core.upgrade_function_for(FakeThing, 3) + def upgrade_one_to_two_three(_data_dict): + return {"foo_3": _data_dict["foo_2"]} + + # @TODO: further discussion required + # not allowed to overwrite registered functions + # with self.assertRaises(ValueError): + # @otio.core.upgrade_function_for(FakeThing, 3) + # def upgrade_one_to_two_three_again(_data_dict): + # raise RuntimeError("shouldn't see this ever") + + ft = otio.core.instance_from_schema("NewStuff", 1, {"foo": "bar"}) + self.assertEqual(ft._dynamic_fields['foo_3'], "bar") + + ft = otio.core.instance_from_schema("NewStuff", 3, {"foo_2": "bar"}) + self.assertEqual(ft._dynamic_fields['foo_3'], "bar") + + ft = otio.core.instance_from_schema("NewStuff", 4, {"foo_3": "bar"}) + self.assertEqual(ft._dynamic_fields['foo_3'], "bar") + + def test_upgrade_rename(self): + """test that upgrading system handles schema renames correctly""" + + @otio.core.register_type + class FakeThingToRename(otio.core.SerializableObject): + _serializable_label = "ThingToRename.2" + my_field = otio.core.serializable_field("my_field", doc="example") + + thing = otio.core.type_version_map() + self.assertTrue(thing) + + def test_downgrade_version(self): + """ test a python defined downgrade function""" + + @otio.core.register_type + class FakeThing(otio.core.SerializableObject): + _serializable_label = "FakeThingToDowngrade.2" + foo_two = otio.core.serializable_field("foo_2") + + @otio.core.downgrade_function_from(FakeThing, 2) + def downgrade_2_to_1(_data_dict): + return {"foo": _data_dict["foo_2"]} + + # @TODO: further discussion required + # # not allowed to overwrite registered functions + # with self.assertRaises(ValueError): + # @otio.core.downgrade_function_from(FakeThing, 2) + # def downgrade_2_to_1_again(_data_dict): + # raise RuntimeError("shouldn't see this ever") + + f = FakeThing() + f.foo_two = "a thing here" + + downgrade_target = {"FakeThingToDowngrade": 1} + + result = json.loads( + otio.adapters.otio_json.write_to_string(f, downgrade_target) + ) + + self.assertDictEqual( + result, + { + "OTIO_SCHEMA": "FakeThingToDowngrade.1", + "foo": "a thing here", + } + ) + + if __name__ == '__main__': unittest.main() diff --git a/tests/test_serialization.cpp b/tests/test_serialization.cpp new file mode 100644 index 000000000..53c352b2c --- /dev/null +++ b/tests/test_serialization.cpp @@ -0,0 +1,117 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Contributors to the OpenTimelineIO project + +#include "utils.h" + +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +namespace otime = opentime::OPENTIME_VERSION; +namespace otio = opentimelineio::OPENTIMELINEIO_VERSION; + +int +main(int argc, char** argv) +{ + Tests tests; + + tests.add_test( + "success with default indent", [] { + otio::SerializableObject::Retainer cl = + new otio::Clip(); + otio::SerializableObject::Retainer tr = + new otio::Track(); + tr->append_child(cl); + otio::SerializableObject::Retainer tl = + new otio::Timeline(); + tl->tracks()->append_child(tr); + + otio::ErrorStatus err; + auto output = tl.value->to_json_string(&err, {}); + assertFalse(otio::is_error(err)); + assertEqual(output.c_str(), R"CONTENT({ + "OTIO_SCHEMA": "Timeline.1", + "metadata": {}, + "name": "", + "global_start_time": null, + "tracks": { + "OTIO_SCHEMA": "Stack.1", + "metadata": {}, + "name": "tracks", + "source_range": null, + "effects": [], + "markers": [], + "enabled": true, + "children": [ + { + "OTIO_SCHEMA": "Track.1", + "metadata": {}, + "name": "", + "source_range": null, + "effects": [], + "markers": [], + "enabled": true, + "children": [ + { + "OTIO_SCHEMA": "Clip.2", + "metadata": {}, + "name": "", + "source_range": null, + "effects": [], + "markers": [], + "enabled": true, + "media_references": { + "DEFAULT_MEDIA": { + "OTIO_SCHEMA": "MissingReference.1", + "metadata": {}, + "name": "", + "available_range": null, + "available_image_bounds": null + } + }, + "active_media_reference_key": "DEFAULT_MEDIA" + } + ], + "kind": "Video" + } + ] + } +})CONTENT"); + }); + + tests.add_test( + "success with indent set to 0", [] { + otio::SerializableObject::Retainer so = + new otio::SerializableObjectWithMetadata(); + + otio::ErrorStatus err; + auto output = so.value->to_json_string(&err, {}, 0); + assertFalse(otio::is_error(err)); + assertEqual(output.c_str(), R"CONTENT({"OTIO_SCHEMA":"SerializableObjectWithMetadata.1","metadata":{},"name":""})CONTENT"); + }); + + tests.add_test( + "success with indent set to 2", [] { + otio::SerializableObject::Retainer so = + new otio::SerializableObjectWithMetadata(); + + otio::ErrorStatus err; + auto output = so.value->to_json_string(&err, {}, 2); + assertFalse(otio::is_error(err)); + assertEqual(output.c_str(), R"CONTENT({ + "OTIO_SCHEMA": "SerializableObjectWithMetadata.1", + "metadata": {}, + "name": "" +})CONTENT"); + }); + + tests.run(argc, argv); + return 0; +} diff --git a/tests/test_serialized_schema.py b/tests/test_serialized_schema.py index 11701f660..7a486f29c 100644 --- a/tests/test_serialized_schema.py +++ b/tests/test_serialized_schema.py @@ -1,33 +1,15 @@ -# +# SPDX-License-Identifier: Apache-2.0 # Copyright Contributors to the OpenTimelineIO project -# -# Licensed under the Apache License, Version 2.0 (the "Apache License") -# with the following modification; you may not use this file except in -# compliance with the Apache License and the following modification to it: -# Section 6. Trademarks. is deleted and replaced with: -# -# 6. Trademarks. This License does not grant permission to use the trade -# names, trademarks, service marks, or product names of the Licensor -# and its affiliates, except as required to comply with Section 4(c) of -# the License and to reproduce the content of the NOTICE file. -# -# You may obtain a copy of the Apache License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the Apache License with the above modification is -# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -# KIND, either express or implied. See the Apache License for the specific -# language governing permissions and limitations under the Apache License. -# import unittest import os +import sys +import subprocess from opentimelineio.console import ( autogen_serialized_datamodel as asd, autogen_plugin_documentation as apd, + autogen_version_map as avm ) @@ -61,7 +43,7 @@ def test_serialized_schema(self): @unittest.skipIf( os.environ.get("OTIO_DISABLE_SERIALIZED_SCHEMA_TEST"), - "Serialized schema test disabled because " + "Plugin documentation test disabled because " "$OTIO_DISABLE_SERIALIZED_SCHEMA_TEST is set to something other than ''" ) class PluginDocumentationTester(unittest.TestCase): @@ -90,5 +72,62 @@ def test_plugin_documentation(self): ) +@unittest.skipIf( + os.environ.get("OTIO_DISABLE_SERIALIZED_SCHEMA_TEST"), + "CORE_VERSION_MAP generation test disabled because " + "$OTIO_DISABLE_SERIALIZED_SCHEMA_TEST is set to something other than ''" +) +class CoreVersionMapGenerationTester(unittest.TestCase): + def test_core_version_map_generator(self): + """Verify the current CORE_VERSION_MAP matches the checked in one.""" + + pt = os.path.dirname(os.path.dirname(__file__)) + root = os.path.join(pt, "src", "opentimelineio") + template_fp = os.path.join(root, "CORE_VERSION_MAP.last.cpp") + target_fp = os.path.join(root, "CORE_VERSION_MAP.cpp") + + with open(target_fp) as fi: + # sanitize line endings and remove empty lines for cross-windows + # /*nix consistent behavior + baseline_text = "\n".join( + ln + for ln in fi.read().splitlines() + if ln + ) + + proc = subprocess.Popen( + [ + sys.executable, + avm.__file__, + "-i", + template_fp, + "-d", + ], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + ) + stdout, _ = proc.communicate() + + test_text = stdout.decode("utf-8")[:-1] + + # sanitize the line endings + test_text = "\n".join( + ln + for ln in test_text.splitlines() + if ln + ) + + self.maxDiff = None + self.longMessage = True + self.assertMultiLineEqual( + baseline_text, + test_text, + "\n The CORE_VERSION_MAP has changed and the autogenerated one in" + " {} needs to be updated. run: `make version-map-update`".format( + target_fp + ) + ) + + if __name__ == '__main__': unittest.main() diff --git a/tests/test_stack_algo.py b/tests/test_stack_algo.py index 92227e8f3..a47da18b0 100644 --- a/tests/test_stack_algo.py +++ b/tests/test_stack_algo.py @@ -1,27 +1,7 @@ #!/usr/bin/env python # +# SPDX-License-Identifier: Apache-2.0 # Copyright Contributors to the OpenTimelineIO project -# -# Licensed under the Apache License, Version 2.0 (the "Apache License") -# with the following modification; you may not use this file except in -# compliance with the Apache License and the following modification to it: -# Section 6. Trademarks. is deleted and replaced with: -# -# 6. Trademarks. This License does not grant permission to use the trade -# names, trademarks, service marks, or product names of the Licensor -# and its affiliates, except as required to comply with Section 4(c) of -# the License and to reproduce the content of the NOTICE file. -# -# You may obtain a copy of the Apache License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the Apache License with the above modification is -# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -# KIND, either express or implied. See the Apache License for the specific -# language governing permissions and limitations under the Apache License. -# """Test file for the stack algorithms library.""" diff --git a/tests/test_svg_adapter.py b/tests/test_svg_adapter.py index dc24fed69..3defe691d 100755 --- a/tests/test_svg_adapter.py +++ b/tests/test_svg_adapter.py @@ -1,26 +1,5 @@ -# +# SPDX-License-Identifier: Apache-2.0 # Copyright Contributors to the OpenTimelineIO project -# -# Licensed under the Apache License, Version 2.0 (the "Apache License") -# with the following modification; you may not use this file except in -# compliance with the Apache License and the following modification to it: -# Section 6. Trademarks. is deleted and replaced with: -# -# 6. Trademarks. This License does not grant permission to use the trade -# names, trademarks, service marks, or product names of the Licensor -# and its affiliates, except as required to comply with Section 4(c) of -# the License and to reproduce the content of the NOTICE file. -# -# You may obtain a copy of the Apache License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the Apache License with the above modification is -# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -# KIND, either express or implied. See the Apache License for the specific -# language governing permissions and limitations under the Apache License. -# """Unit tests for the OTIO to SVG adapter""" diff --git a/tests/test_timeline.cpp b/tests/test_timeline.cpp new file mode 100644 index 000000000..b8022a530 --- /dev/null +++ b/tests/test_timeline.cpp @@ -0,0 +1,83 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Contributors to the OpenTimelineIO project + +#include "utils.h" + +#include +#include +#include + +#include + +namespace otime = opentime::OPENTIME_VERSION; +namespace otio = opentimelineio::OPENTIMELINEIO_VERSION; + +int +main(int argc, char** argv) +{ + Tests tests; + + tests.add_test( + "test_find_children", [] { + using namespace otio; + otio::SerializableObject::Retainer cl = + new otio::Clip(); + otio::SerializableObject::Retainer tr = + new otio::Track(); + tr->append_child(cl); + otio::SerializableObject::Retainer tl = + new otio::Timeline(); + tl->tracks()->append_child(tr); + opentimelineio::v1_0::ErrorStatus err; + auto result = tl->find_children(&err); + assertEqual(result.size(), 1); + assertEqual(result[0].value, cl.value); + }); + tests.add_test( + "test_find_children_search_range", [] { + using namespace otio; + const TimeRange range(RationalTime(0.0, 24.0), RationalTime(24.0, 24.0)); + otio::SerializableObject::Retainer cl0 = + new otio::Clip(); + cl0->set_source_range(range); + otio::SerializableObject::Retainer cl1 = + new otio::Clip(); + cl1->set_source_range(range); + otio::SerializableObject::Retainer cl2 = + new otio::Clip(); + cl2->set_source_range(range); + otio::SerializableObject::Retainer tr = + new otio::Track(); + tr->append_child(cl0); + tr->append_child(cl1); + tr->append_child(cl2); + otio::SerializableObject::Retainer tl = + new otio::Timeline(); + tl->tracks()->append_child(tr); + opentimelineio::v1_0::ErrorStatus err; + auto result = tl->find_children(&err, range); + assertEqual(result.size(), 1); + assertEqual(result[0].value, cl0.value); + }); + tests.add_test( + "test_find_children_shallow_search", [] { + using namespace otio; + otio::SerializableObject::Retainer cl = + new otio::Clip(); + otio::SerializableObject::Retainer tr = + new otio::Track(); + tr->append_child(cl); + otio::SerializableObject::Retainer tl = + new otio::Timeline(); + tl->tracks()->append_child(tr); + opentimelineio::v1_0::ErrorStatus err; + auto result = tl->find_children(&err, nullopt, true); + assertEqual(result.size(), 0); + result = tl->find_children(&err, nullopt, false); + assertEqual(result.size(), 1); + assertEqual(result[0].value, cl.value); + }); + + tests.run(argc, argv); + return 0; +} diff --git a/tests/test_timeline.py b/tests/test_timeline.py index 7239f463a..e50eb8b05 100755 --- a/tests/test_timeline.py +++ b/tests/test_timeline.py @@ -1,42 +1,13 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- - -# +# SPDX-License-Identifier: Apache-2.0 # Copyright Contributors to the OpenTimelineIO project -# -# Licensed under the Apache License, Version 2.0 (the "Apache License") -# with the following modification; you may not use this file except in -# compliance with the Apache License and the following modification to it: -# Section 6. Trademarks. is deleted and replaced with: -# -# 6. Trademarks. This License does not grant permission to use the trade -# names, trademarks, service marks, or product names of the Licensor -# and its affiliates, except as required to comply with Section 4(c) of -# the License and to reproduce the content of the NOTICE file. -# -# You may obtain a copy of the Apache License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the Apache License with the above modification is -# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -# KIND, either express or implied. See the Apache License for the specific -# language governing permissions and limitations under the Apache License. -# import math import os import sys import unittest -# handle python2 vs python3 difference -try: - from tempfile import TemporaryDirectory # noqa: F401 - import tempfile -except ImportError: - # XXX: python2.7 only - from backports import tempfile +import tempfile import opentimelineio as otio import opentimelineio.test_utils as otio_test_utils @@ -81,18 +52,14 @@ def test_big_integers(self): self.assertEqual(md["neg_smallest_int64"], -2147483648) self.assertEqual(md["negverybig"], -3450100000) - big_int_type = int - if sys.version_info[0] < 3: - big_int_type = long # noqa: F821 - # from memory supported_integers = [ # name value to enter - ('minint32', -big_int_type(2**31 - 1)), - ('maxint32', big_int_type(2**31 - 1)), - ('maxuint32', big_int_type(2**32 - 1)), - ('minint64', -big_int_type(2**63 - 1)), - ('maxint64', big_int_type(2**63 - 1)), + ('minint32', -int(2**31 - 1)), + ('maxint32', int(2**31 - 1)), + ('maxuint32', int(2**32 - 1)), + ('minint64', -int(2**63 - 1)), + ('maxint64', int(2**63 - 1)), ] for (name, value) in supported_integers: @@ -108,11 +75,11 @@ def test_big_integers(self): ) self.assertEqual( type(md[name]), - big_int_type, + int, "{} didn't match expected type: got {} expected {}".format( name, type(md[name]), - big_int_type + int ) ) @@ -183,7 +150,7 @@ def test_big_integers(self): else: self.assertTrue( math.isnan(md[name]), - "Expected {} to be a nan, got {}".format(name, md[name]) + f"Expected {name} to be a nan, got {md[name]}" ) self.assertEqual( @@ -268,7 +235,7 @@ def test_big_integers(self): for (name, value, exc) in unsupported_values: with self.assertRaises( exc, - msg="Expected {} to raise an exception.".format(name) + msg=f"Expected {name} to raise an exception." ): md[name] = value @@ -279,10 +246,6 @@ def test_unicode(self): utf8_test_str = "Viel glück und hab spaß!" - # python2 - if sys.version_info[0] < 3: - utf8_test_str = utf8_test_str.decode('utf8') - self.assertEqual(md['utf8'], utf8_test_str) tl = otio.schema.Timeline() @@ -295,7 +258,6 @@ def test_unicode(self): self.assertIsOTIOEquivalentTo(tl, decoded) self.assertEqual(tl.metadata, decoded.metadata) - @unittest.skipIf(sys.version_info < (3, 0), "unicode does funny things in python2") def test_unicode_file_name(self): with tempfile.TemporaryDirectory() as temp_dir: @@ -430,27 +392,27 @@ def test_iterators(self): ) tl.tracks[0].append(cl) tl.tracks[0].extend([cl2, cl3]) - self.assertEqual([cl, cl2, cl3], list(tl.each_clip())) + self.assertEqual([cl, cl2, cl3], list(tl.find_clips())) rt_start = otio.opentime.RationalTime(0, 24) rt_end = otio.opentime.RationalTime(1, 24) search_range = otio.opentime.TimeRange(rt_start, rt_end) - self.assertEqual([cl], list(tl.each_clip(search_range))) + self.assertEqual([cl], list(tl.find_clips(search_range))) # check to see if full range works search_range = tl.tracks.trimmed_range() - self.assertEqual([cl, cl2, cl3], list(tl.each_clip(search_range))) + self.assertEqual([cl, cl2, cl3], list(tl.find_clips(search_range))) # just one clip search_range = cl2.range_in_parent() - self.assertEqual([cl2], list(tl.each_clip(search_range))) + self.assertEqual([cl2], list(tl.find_clips(search_range))) # the last two clips search_range = otio.opentime.TimeRange( start_time=cl2.range_in_parent().start_time, duration=cl2.trimmed_range().duration + rt_end ) - self.assertEqual([cl2, cl3], list(tl.each_clip(search_range))) + self.assertEqual([cl2, cl3], list(tl.find_clips(search_range))) # no clips search_range = otio.opentime.TimeRange( @@ -460,7 +422,7 @@ def test_iterators(self): ), duration=rt_end ) - self.assertEqual([], list(tl.each_clip(search_range))) + self.assertEqual([], list(tl.find_clips(search_range))) def test_str(self): self.maxDiff = None @@ -579,6 +541,48 @@ def test_tracks_set_null_tracks(self): self.assertEqual(len(tl.video_tracks()), 0) self.assertTrue(isinstance(tl.tracks, otio.schema.Stack)) + def test_find_children(self): + cl = otio.schema.Clip() + tr = otio.schema.Track() + tr.append(cl) + tl = otio.schema.Timeline() + tl.tracks.append(tr) + result = tl.find_children(otio.schema.Clip) + self.assertEqual(len(result), 1) + self.assertEqual(result[0], cl) + + def test_find_children_search_range(self): + range = otio.opentime.TimeRange( + otio.opentime.RationalTime(0.0, 24.0), + otio.opentime.RationalTime(24.0, 24.0)) + cl0 = otio.schema.Clip() + cl0.source_range = range + cl1 = otio.schema.Clip() + cl1.source_range = range + cl2 = otio.schema.Clip() + cl2.source_range = range + tr = otio.schema.Track() + tr.append(cl0) + tr.append(cl1) + tr.append(cl2) + tl = otio.schema.Timeline() + tl.tracks.append(tr) + result = tl.find_children(otio.schema.Clip, range) + self.assertEqual(len(result), 1) + self.assertEqual(result[0], cl0) + + def test_find_children_shallow_search(self): + cl = otio.schema.Clip() + tr = otio.schema.Track() + tr.append(cl) + tl = otio.schema.Timeline() + tl.tracks.append(tr) + result = tl.find_children(otio.schema.Clip, shallow_search=True) + self.assertEqual(len(result), 0) + result = tl.find_children(otio.schema.Clip, shallow_search=False) + self.assertEqual(len(result), 1) + self.assertEqual(result[0], cl) + if __name__ == '__main__': unittest.main() diff --git a/tests/test_timeline_algo.py b/tests/test_timeline_algo.py index a21f4db7d..8153b5c2e 100644 --- a/tests/test_timeline_algo.py +++ b/tests/test_timeline_algo.py @@ -1,27 +1,7 @@ #!/usr/bin/env python # +# SPDX-License-Identifier: Apache-2.0 # Copyright Contributors to the OpenTimelineIO project -# -# Licensed under the Apache License, Version 2.0 (the "Apache License") -# with the following modification; you may not use this file except in -# compliance with the Apache License and the following modification to it: -# Section 6. Trademarks. is deleted and replaced with: -# -# 6. Trademarks. This License does not grant permission to use the trade -# names, trademarks, service marks, or product names of the Licensor -# and its affiliates, except as required to comply with Section 4(c) of -# the License and to reproduce the content of the NOTICE file. -# -# You may obtain a copy of the Apache License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the Apache License with the above modification is -# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -# KIND, either express or implied. See the Apache License for the specific -# language governing permissions and limitations under the Apache License. -# """Test file for the track algorithms library.""" diff --git a/tests/test_track.cpp b/tests/test_track.cpp new file mode 100644 index 000000000..96ae06145 --- /dev/null +++ b/tests/test_track.cpp @@ -0,0 +1,113 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Contributors to the OpenTimelineIO project + +#include "utils.h" + +#include +#include +#include + +#include + +namespace otime = opentime::OPENTIME_VERSION; +namespace otio = opentimelineio::OPENTIMELINEIO_VERSION; + +int +main(int argc, char** argv) +{ + Tests tests; + + tests.add_test( + "test_find_children", [] { + using namespace otio; + otio::SerializableObject::Retainer cl = + new otio::Clip(); + otio::SerializableObject::Retainer tr = + new otio::Track(); + tr->append_child(cl); + opentimelineio::v1_0::ErrorStatus err; + auto result = tr->find_children(&err); + assertEqual(result.size(), 1); + assertEqual(result[0].value, cl.value); + }); + tests.add_test( + "test_find_children_search_range", [] { + using namespace otio; + const TimeRange range(RationalTime(0.0, 24.0), RationalTime(24.0, 24.0)); + otio::SerializableObject::Retainer cl0 = + new otio::Clip(); + cl0->set_source_range(range); + otio::SerializableObject::Retainer cl1 = + new otio::Clip(); + cl1->set_source_range(range); + otio::SerializableObject::Retainer cl2 = + new otio::Clip(); + cl2->set_source_range(range); + otio::SerializableObject::Retainer tr = + new otio::Track(); + tr->append_child(cl0); + tr->append_child(cl1); + tr->append_child(cl2); + opentimelineio::v1_0::ErrorStatus err; + auto result = tr->find_children( + &err, + TimeRange(RationalTime(0.0, 24.0), RationalTime(24.0, 24.0))); + assertEqual(result.size(), 1); + assertEqual(result[0].value, cl0.value); + result = tr->find_children( + &err, + TimeRange(RationalTime(24.0, 24.0), RationalTime(24.0, 24.0))); + assertEqual(result.size(), 1); + assertEqual(result[0].value, cl1.value); + result = tr->find_children( + &err, + TimeRange(RationalTime(48.0, 24.0), RationalTime(24.0, 24.0))); + assertEqual(result.size(), 1); + assertEqual(result[0].value, cl2.value); + result = tr->find_children( + &err, + TimeRange(RationalTime(0.0, 24.0), RationalTime(48.0, 24.0))); + assertEqual(result.size(), 2); + assertEqual(result[0].value, cl0.value); + assertEqual(result[1].value, cl1.value); + result = tr->find_children( + &err, + TimeRange(RationalTime(24.0, 24.0), RationalTime(48.0, 24.0))); + assertEqual(result.size(), 2); + assertEqual(result[0].value, cl1.value); + assertEqual(result[1].value, cl2.value); + result = tr->find_children( + &err, + TimeRange(RationalTime(0.0, 24.0), RationalTime(72.0, 24.0))); + assertEqual(result.size(), 3); + assertEqual(result[0].value, cl0.value); + assertEqual(result[1].value, cl1.value); + assertEqual(result[2].value, cl2.value); + }); + tests.add_test( + "test_find_children_shallow_search", [] { + using namespace otio; + otio::SerializableObject::Retainer cl0 = + new otio::Clip(); + otio::SerializableObject::Retainer cl1 = + new otio::Clip(); + otio::SerializableObject::Retainer st = + new otio::Stack(); + st->append_child(cl1); + otio::SerializableObject::Retainer tr = + new otio::Track(); + tr->append_child(cl0); + tr->append_child(st); + opentimelineio::v1_0::ErrorStatus err; + auto result = tr->find_children(&err, nullopt, true); + assertEqual(result.size(), 1); + assertEqual(result[0].value, cl0.value); + result = tr->find_children(&err, nullopt, false); + assertEqual(result.size(), 2); + assertEqual(result[0].value, cl0.value); + assertEqual(result[1].value, cl1.value); + }); + + tests.run(argc, argv); + return 0; +} diff --git a/tests/test_track.py b/tests/test_track.py new file mode 100644 index 000000000..fb2caccc7 --- /dev/null +++ b/tests/test_track.py @@ -0,0 +1,57 @@ +#!/usr/bin/env python +# SPDX-License-Identifier: Apache-2.0 +# Copyright Contributors to the OpenTimelineIO project + +import unittest + +import opentimelineio as otio +import opentimelineio.test_utils as otio_test_utils + + +class TrackTests(unittest.TestCase, otio_test_utils.OTIOAssertions): + + def test_find_children(self): + cl = otio.schema.Clip() + tr = otio.schema.Track() + tr.append(cl) + result = tr.find_children(otio.schema.Clip) + self.assertEqual(len(result), 1) + self.assertEqual(result[0], cl) + + def test_find_children_search_range(self): + range = otio.opentime.TimeRange( + otio.opentime.RationalTime(0.0, 24.0), + otio.opentime.RationalTime(24.0, 24.0)) + cl0 = otio.schema.Clip() + cl0.source_range = range + cl1 = otio.schema.Clip() + cl1.source_range = range + cl2 = otio.schema.Clip() + cl2.source_range = range + tr = otio.schema.Track() + tr.append(cl0) + tr.append(cl1) + tr.append(cl2) + result = tr.find_children(otio.schema.Clip, range) + self.assertEqual(len(result), 1) + self.assertEqual(result[0], cl0) + + def test_find_children_shallow_search(self): + cl0 = otio.schema.Clip() + cl1 = otio.schema.Clip() + st = otio.schema.Stack() + st.append(cl1) + tr = otio.schema.Track() + tr.append(cl0) + tr.append(st) + result = tr.find_children(otio.schema.Clip, shallow_search=True) + self.assertEqual(len(result), 1) + self.assertEqual(result[0], cl0) + result = tr.find_children(otio.schema.Clip, shallow_search=False) + self.assertEqual(len(result), 2) + self.assertEqual(result[0], cl0) + self.assertEqual(result[1], cl1) + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/test_track_algo.py b/tests/test_track_algo.py index aa9f32c85..f3313c124 100644 --- a/tests/test_track_algo.py +++ b/tests/test_track_algo.py @@ -1,27 +1,7 @@ #!/usr/bin/env python # +# SPDX-License-Identifier: Apache-2.0 # Copyright Contributors to the OpenTimelineIO project -# -# Licensed under the Apache License, Version 2.0 (the "Apache License") -# with the following modification; you may not use this file except in -# compliance with the Apache License and the following modification to it: -# Section 6. Trademarks. is deleted and replaced with: -# -# 6. Trademarks. This License does not grant permission to use the trade -# names, trademarks, service marks, or product names of the Licensor -# and its affiliates, except as required to comply with Section 4(c) of -# the License and to reproduce the content of the NOTICE file. -# -# You may obtain a copy of the Apache License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the Apache License with the above modification is -# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -# KIND, either express or implied. See the Apache License for the specific -# language governing permissions and limitations under the Apache License. -# """Test file for the track algorithms library.""" diff --git a/tests/test_transition.py b/tests/test_transition.py index 36bc5ee0b..fdf78c1c1 100644 --- a/tests/test_transition.py +++ b/tests/test_transition.py @@ -1,26 +1,5 @@ -# +# SPDX-License-Identifier: Apache-2.0 # Copyright Contributors to the OpenTimelineIO project -# -# Licensed under the Apache License, Version 2.0 (the "Apache License") -# with the following modification; you may not use this file except in -# compliance with the Apache License and the following modification to it: -# Section 6. Trademarks. is deleted and replaced with: -# -# 6. Trademarks. This License does not grant permission to use the trade -# names, trademarks, service marks, or product names of the Licensor -# and its affiliates, except as required to comply with Section 4(c) of -# the License and to reproduce the content of the NOTICE file. -# -# You may obtain a copy of the Apache License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the Apache License with the above modification is -# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -# KIND, either express or implied. See the Apache License for the specific -# language governing permissions and limitations under the Apache License. -# """Transition class test harness.""" @@ -95,6 +74,91 @@ def test_stringify(self): ) ) + def test_setters(self): + trx = otio.schema.Transition( + name="AtoB", + transition_type="SMPTE.Dissolve", + metadata={ + "foo": "bar" + } + ) + self.assertEqual(trx.transition_type, "SMPTE.Dissolve") + trx.transition_type = "EdgeWipe" + self.assertEqual(trx.transition_type, "EdgeWipe") + + def test_parent_range(self): + timeline = otio.schema.Timeline( + tracks=[ + otio.schema.Track( + name="V1", + children=[ + otio.schema.Clip( + name="A", + source_range=otio.opentime.TimeRange( + start_time=otio.opentime.RationalTime( + value=1, + rate=30 + ), + duration=otio.opentime.RationalTime( + value=50, + rate=30 + ) + ) + ), + otio.schema.Transition( + in_offset=otio.opentime.RationalTime( + value=7, + rate=30 + ), + out_offset=otio.opentime.RationalTime( + value=10, + rate=30 + ), + ), + otio.schema.Clip( + name="B", + source_range=otio.opentime.TimeRange( + start_time=otio.opentime.RationalTime( + value=100, + rate=30 + ), + duration=otio.opentime.RationalTime( + value=50, + rate=30 + ) + ) + ), + ] + ) + ] + ) + trx = timeline.tracks[0][1] + time_range = otio.opentime.TimeRange(otio.opentime.RationalTime(43, 30), + otio.opentime.RationalTime(17, 30)) + + self.assertEqual(time_range, + trx.range_in_parent()) + + self.assertEqual(time_range, + trx.trimmed_range_in_parent()) + + trx = otio.schema.Transition( + in_offset=otio.opentime.RationalTime( + value=7, + rate=30 + ), + out_offset=otio.opentime.RationalTime( + value=10, + rate=30 + ), + ) + + with self.assertRaises(otio.exceptions.NotAChildError): + trx.range_in_parent() + + with self.assertRaises(otio.exceptions.NotAChildError): + trx.trimmed_range_in_parent() + if __name__ == '__main__': unittest.main() diff --git a/tests/test_unknown_schema.py b/tests/test_unknown_schema.py index fa0fdaab4..54e2b89b6 100644 --- a/tests/test_unknown_schema.py +++ b/tests/test_unknown_schema.py @@ -1,26 +1,5 @@ -# +# SPDX-License-Identifier: Apache-2.0 # Copyright Contributors to the OpenTimelineIO project -# -# Licensed under the Apache License, Version 2.0 (the "Apache License") -# with the following modification; you may not use this file except in -# compliance with the Apache License and the following modification to it: -# Section 6. Trademarks. is deleted and replaced with: -# -# 6. Trademarks. This License does not grant permission to use the trade -# names, trademarks, service marks, or product names of the Licensor -# and its affiliates, except as required to comply with Section 4(c) of -# the License and to reproduce the content of the NOTICE file. -# -# You may obtain a copy of the Apache License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the Apache License with the above modification is -# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -# KIND, either express or implied. See the Apache License for the specific -# language governing permissions and limitations under the Apache License. -# import unittest diff --git a/tests/test_url_conversions.py b/tests/test_url_conversions.py index 2d6277488..5e994148b 100644 --- a/tests/test_url_conversions.py +++ b/tests/test_url_conversions.py @@ -1,26 +1,5 @@ -# +# SPDX-License-Identifier: Apache-2.0 # Copyright Contributors to the OpenTimelineIO project -# -# Licensed under the Apache License, Version 2.0 (the "Apache License") -# with the following modification; you may not use this file except in -# compliance with the Apache License and the following modification to it: -# Section 6. Trademarks. is deleted and replaced with: -# -# 6. Trademarks. This License does not grant permission to use the trade -# names, trademarks, service marks, or product names of the Licensor -# and its affiliates, except as required to comply with Section 4(c) of -# the License and to reproduce the content of the NOTICE file. -# -# You may obtain a copy of the Apache License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the Apache License with the above modification is -# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -# KIND, either express or implied. See the Apache License for the specific -# language governing permissions and limitations under the Apache License. -# """ Unit tests of functions that convert between file paths and urls. """ @@ -70,7 +49,7 @@ def test_roundtrip_rel(self): result = otio.url_utils.filepath_from_url(MEDIA_EXAMPLE_PATH_URL_REL) # should have reconstructed it by this point - self.assertEqual(result, MEDIA_EXAMPLE_PATH_REL) + self.assertEqual(os.path.normpath(result), MEDIA_EXAMPLE_PATH_REL) if __name__ == "__main__": diff --git a/tests/test_v2d.py b/tests/test_v2d.py index 102870bfd..69f9c3f67 100644 --- a/tests/test_v2d.py +++ b/tests/test_v2d.py @@ -1,26 +1,5 @@ -# +# SPDX-License-Identifier: Apache-2.0 # Copyright Contributors to the OpenTimelineIO project -# -# Licensed under the Apache License, Version 2.0 (the "Apache License") -# with the following modification; you may not use this file except in -# compliance with the Apache License and the following modification to it: -# Section 6. Trademarks. is deleted and replaced with: -# -# 6. Trademarks. This License does not grant permission to use the trade -# names, trademarks, service marks, or product names of the Licensor -# and its affiliates, except as required to comply with Section 4(c) of -# the License and to reproduce the content of the NOTICE file. -# -# You may obtain a copy of the Apache License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the Apache License with the above modification is -# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -# KIND, either express or implied. See the Apache License for the specific -# language governing permissions and limitations under the Apache License. -# import unittest import sys diff --git a/tests/test_version_manifest.py b/tests/test_version_manifest.py new file mode 100644 index 000000000..4fecd638e --- /dev/null +++ b/tests/test_version_manifest.py @@ -0,0 +1,148 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright Contributors to the OpenTimelineIO project + +"""unit tests for the version manifest plugin system""" + +import unittest +import os +import json + +import opentimelineio as otio +from tests import utils + + +FIRST_MANIFEST = """{ + "OTIO_SCHEMA" : "PluginManifest.1", + "version_manifests": { + "UNIQUE_FAMILY": { + "TEST_LABEL": { + "second_thing": 3 + } + }, + "LAYERED_FAMILY": { + "June2022": { + "SimpleClass": 2 + }, + "May2022": { + "SimpleClass": 1 + } + } + } +} +""" + +SECOND_MANIFEST = """{ + "OTIO_SCHEMA" : "PluginManifest.1", + "version_manifests": { + "LAYERED_FAMILY": { + "May2022": { + "SimpleClass": 2 + }, + "April2022": { + "SimpleClass": 1 + } + } + } +} +""" + + +class TestPlugin_VersionManifest(unittest.TestCase): + def setUp(self): + self.bak = otio.plugins.ActiveManifest() + self.man = utils.create_manifest() + otio.plugins.manifest._MANIFEST = self.man + + if "OTIO_DEFAULT_TARGET_VERSION_FAMILY_LABEL" in os.environ: + del os.environ["OTIO_DEFAULT_TARGET_VERSION_FAMILY_LABEL"] + + def tearDown(self): + otio.plugins.manifest._MANIFEST = self.bak + utils.remove_manifest(self.man) + if "OTIO_DEFAULT_TARGET_VERSION_FAMILY_LABEL" in os.environ: + del os.environ["OTIO_DEFAULT_TARGET_VERSION_FAMILY_LABEL"] + + def test_read_in_manifest(self): + self.assertIn("TEST_FAMILY_NAME", self.man.version_manifests) + self.assertIn( + "TEST_LABEL", + self.man.version_manifests["TEST_FAMILY_NAME"] + ) + + def test_full_map(self): + d = otio.versioning.full_map() + self.assertIn("TEST_FAMILY_NAME", d) + self.assertIn( + "TEST_LABEL", + d["TEST_FAMILY_NAME"] + ) + + def test_fetch_map(self): + self.assertEqual( + otio.versioning.fetch_map("TEST_FAMILY_NAME", "TEST_LABEL"), + {"ExampleSchema": 2, "EnvVarTestSchema": 1, "Clip": 1} + ) + + def test_env_variable_downgrade(self): + @otio.core.register_type + class EnvVarTestSchema(otio.core.SerializableObject): + _serializable_label = "EnvVarTestSchema.2" + foo_two = otio.core.serializable_field("foo_2") + + @otio.core.downgrade_function_from(EnvVarTestSchema, 2) + def downgrade_2_to_1(_data_dict): + return {"foo": _data_dict["foo_2"]} + + evt = EnvVarTestSchema() + evt.foo_two = "asdf" + + result = json.loads(otio.adapters.otio_json.write_to_string(evt)) + self.assertEqual(result["OTIO_SCHEMA"], "EnvVarTestSchema.2") + + # env variable should make a downgrade by default... + os.environ["OTIO_DEFAULT_TARGET_VERSION_FAMILY_LABEL"] = ( + "TEST_FAMILY_NAME:TEST_LABEL" + ) + result = json.loads(otio.adapters.otio_json.write_to_string(evt)) + self.assertEqual(result["OTIO_SCHEMA"], "EnvVarTestSchema.1") + + # ...but can still be overridden by passing in an argument + result = json.loads(otio.adapters.otio_json.write_to_string(evt, {})) + self.assertEqual(result["OTIO_SCHEMA"], "EnvVarTestSchema.2") + + def test_garbage_env_variables(self): + cl = otio.schema.Clip() + invalid_env_error = otio.exceptions.InvalidEnvironmentVariableError + + # missing ":" + os.environ["OTIO_DEFAULT_TARGET_VERSION_FAMILY_LABEL"] = ( + "invalid_formatting" + ) + with self.assertRaises(invalid_env_error): + otio.adapters.otio_json.write_to_string(cl) + + # asking for family/label that doesn't exist in the plugins + os.environ["OTIO_DEFAULT_TARGET_VERSION_FAMILY_LABEL"] = ( + "nosuch:labelorfamily" + ) + with self.assertRaises(invalid_env_error): + otio.adapters.otio_json.write_to_string(cl) + + def test_two_version_manifests(self): + """test that two manifests layer correctly""" + + fst = otio.plugins.manifest.manifest_from_string(FIRST_MANIFEST) + snd = otio.plugins.manifest.manifest_from_string(SECOND_MANIFEST) + fst.extend(snd) + + self.assertIn("UNIQUE_FAMILY", fst.version_manifests) + + lay_fam = fst.version_manifests["LAYERED_FAMILY"] + + self.assertIn("June2022", lay_fam) + self.assertIn("April2022", lay_fam) + self.assertEqual(lay_fam["May2022"]["SimpleClass"], 2) + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/utils.cpp b/tests/utils.cpp index d0b0df5a7..e49998daa 100644 --- a/tests/utils.cpp +++ b/tests/utils.cpp @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Contributors to the OpenTimelineIO project + #include "utils.h" #include diff --git a/tests/utils.h b/tests/utils.h index 42ab226c5..0a519f08a 100644 --- a/tests/utils.h +++ b/tests/utils.h @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Contributors to the OpenTimelineIO project + #pragma once #undef NDEBUG diff --git a/tests/utils.py b/tests/utils.py index 82f74d582..ecba5deab 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -1,29 +1,7 @@ -# +# SPDX-License-Identifier: Apache-2.0 # Copyright Contributors to the OpenTimelineIO project -# -# Licensed under the Apache License, Version 2.0 (the "Apache License") -# with the following modification; you may not use this file except in -# compliance with the Apache License and the following modification to it: -# Section 6. Trademarks. is deleted and replaced with: -# -# 6. Trademarks. This License does not grant permission to use the trade -# names, trademarks, service marks, or product names of the Licensor -# and its affiliates, except as required to comply with Section 4(c) of -# the License and to reproduce the content of the NOTICE file. -# -# You may obtain a copy of the Apache License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the Apache License with the above modification is -# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -# KIND, either express or implied. See the Apache License for the specific -# language governing permissions and limitations under the Apache License. -# """Reusable utilities for tests.""" -from __future__ import absolute_import # import built-in modules import os