Skip to content

chore: remove Python 2 compatibility code - #651

Open
hdd wants to merge 1 commit into
mainfrom
backlog/remove-py2-compatibility
Open

chore: remove Python 2 compatibility code#651
hdd wants to merge 1 commit into
mainfrom
backlog/remove-py2-compatibility

Conversation

@hdd

@hdd hdd commented Jun 1, 2026

Copy link
Copy Markdown
Collaborator

cleanup from remaning py2 compatibility code paths and imports

Summary by CodeRabbit

  • Refactor

    • Modernized internal code handling and formatting across core modules.
    • Removed legacy compatibility code.
  • Chores

    • Updated code style and formatting standards.

Note: These changes are internal improvements with no impact on user-facing functionality.

Remove all Python 2/3 compatibility shims as the codebase requires
Python 3.13:

- Remove `six` dependency and imports
- Replace `six.string_types` with `str`
- Remove `six.PY2` conditional branches
- Replace `dict.iteritems()` with `dict.items()`
- Replace `unicode` type checks with `str`/`bytes`
- Simplify base64 encoding/decoding (no PY2 path needed)
- Replace `type(x) == str` with `isinstance(x, str)`

The codebase now uses only Python 3 idioms throughout.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
@hdd
hdd requested a review from dennisweil June 1, 2026 09:45
@hdd
hdd requested a review from a team as a code owner June 1, 2026 09:45
@coderabbitai

coderabbitai Bot commented Jun 1, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

This PR modernizes the codebase to remove Python 2 compatibility layers via six, updates text-based JSON/base64 encoding for asset options, removes legacy branching logic, and standardizes on Python 3-native patterns. Changes span asset info encoding, client initialization/execution, string utilities, and exception handling.

Changes

Python 3 Modernization Across Modules

Layer / File(s) Summary
Asset info encoding/decoding and option normalization
libs/framework-core/source/ftrack_framework_core/asset/asset_info.py
Refactors FtrackAssetInfo to standardize on UTF-8 + base64 ASCII text encoding, normalizes string "None" to None via _conform_data, removes Python 2 branching in encode_options/decode_options, and updates dependency detection from type(...) == str to isinstance(..., str).
Asset info factory method refactor
libs/framework-core/source/ftrack_framework_core/asset/asset_info.py
Refactors create classmethod to build context_path from asset ancestry, conditionally select component filesystem path and id based on availability, construct asset_info_data with id generation, version parsing, and dependency ID extraction.
Client initialization and host discovery setup
libs/framework-core/source/ftrack_framework_core/client/__init__.py
Removes six.string_types import, modernizes context_id type validation to isinstance(context_id, str), adds debug logging on host connection assignment, refactors logger initialization, and updates discover_host() loop with modern event data access patterns.
Client tool and dialog execution flow
libs/framework-core/source/ftrack_framework_core/client/__init__.py
Updates tool/dialog execution to access event data via event["data"] keys, iterates tool configs using options.get("tool_configs"), refactors run_dialog validation/registration logic and docking behavior to check dialog_options.get("docked") and dock_func.
Client configuration and plugin management
libs/framework-core/source/ftrack_framework_core/client/__init__.py
Modernizes docstrings for callback wiring, config options assignment, UI hook execution, and plugin verification while preserving option storage under _tool_config_options and item_reference keys.
String utility function modernization
libs/utils/source/ftrack_utils/string/__init__.py
Removes six.PY2 branching in safe_string, updates str_context and str_version to use double-quoted literals and consistent format(...).replace(...) patterns, removes legacy unicode encoding branch.
Exception class Python 3 updates
projects/nuke-studio/source/ftrack_nuke_studio/exception.py
Updates Error and subclasses to use double-quoted docstrings, removes Python 2 iteritems() and unicode encoding, modernizes Error.__str__ to decode bytes via sys.getfilesystemencoding() before message formatting.

🎯 3 (Moderate) | ⏱️ ~25 minutes

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Description check ⚠️ Warning The PR description is minimal and lacks required template sections such as Purpose, Scope, Implementation Details, Testing, and checklist items. While it conveys the general intent, it does not follow the repository's description template structure. Expand the description to follow the template: add Purpose and Scope sections, describe the implementation approach and reasoning, list any tests added, document manual testing performed, and complete the provided checklist items.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the primary change: removal of Python 2 compatibility code across the codebase. It is concise, clear, and directly reflects the main objective of the PR.
Docstring Coverage ✅ Passed Docstring coverage is 98.04% which is sufficient. The required threshold is 50.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch backlog/remove-py2-compatibility

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions

github-actions Bot commented Jun 1, 2026

Copy link
Copy Markdown

Coverage report

This PR does not seem to contain any modification to coverable code.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
libs/framework-core/source/ftrack_framework_core/asset/asset_info.py (1)

124-129: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Replace eval() in _check_asset_info_dependencies

dict(eval(dependency)) in libs/framework-core/source/ftrack_framework_core/asset/asset_info.py can execute arbitrary code if any dependency string is attacker-controlled. Within this repo, _check_asset_info_dependencies has no other call sites, and create() only builds constants.asset.DEPENDENCY_IDS from asset_version_entity["uses_versions"][...]["id"] values (not stringified dicts), but the unsafe eval still needs removal.

🛡️ Safer parsing
+import ast
...
-            if isinstance(dependency, str):
-                dict_dept = dict(eval(dependency))
+            if isinstance(dependency, str):
+                dict_dept = dict(ast.literal_eval(dependency))
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@libs/framework-core/source/ftrack_framework_core/asset/asset_info.py` around
lines 124 - 129, The code in _check_asset_info_dependencies uses
eval(dependency) which is unsafe; replace it with a safe parser such as
ast.literal_eval (import ast) or json.loads depending on the expected input,
e.g. parse the dependency string into a dict via ast.literal_eval and then pass
that dict to FtrackAssetInfo (retain the existing variables dict_dept and
dep_asset_info). Also add minimal exception handling around the parse to catch
ValueError/SyntaxError (log/raise as appropriate) so malformed strings don't
silently crash.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@libs/framework-core/source/ftrack_framework_core/asset/asset_info.py`:
- Around line 63-65: The decode_options path logs an error when
asset_info_options is empty but then continues and causes json.JSONDecodeError;
update the method (the block handling asset_info_options in decode_options) to
return early with a safe default (e.g., {} or None) immediately after logging
the error so it doesn't call base64.b64decode/json.loads on an empty string;
ensure the change references the asset_info_options variable and uses
self.logger.error in the same method to preserve the existing log behavior.

In `@libs/framework-core/source/ftrack_framework_core/client/__init__.py`:
- Line 385: The loop over options.get("tool_configs") can raise TypeError when
tool_configs is None; update the iterator in the block that contains the line
'for tool_config_name in options.get("tool_configs")' to guard against None
(e.g., use options.get("tool_configs", []) or check and set tool_configs =
options.get("tool_configs") or [] before the loop) so the code safely handles
missing or null tool_configs values.
- Line 478: The condition uses dialog_options.get("docked") but dialog_options
can be None; update the check to guard for None before calling .get(), for
example change the conditional around the existing logic to ensure
dialog_options is truthy (e.g. if dialog_options and
dialog_options.get("docked") and dock_func) or use a safe fallback (e.g. if
(dialog_options or {}).get("docked") and dock_func) so dialog_options.get is
never invoked on None; modify the conditional that currently reads `if
dialog_options.get("docked") and dock_func:` accordingly.
- Around line 426-435: The validation incorrectly uses isinstance(dialog_class,
FrameworkDialog) which rejects valid dialog classes; update the check in
Client.run_dialog (the dialog_class validation block) to verify that
dialog_class is a class and a subclass of FrameworkDialog (e.g., use
inspect.isclass(dialog_class) and issubclass(dialog_class, FrameworkDialog)) and
log/raise the same error if that test fails; ensure you import inspect if needed
and preserve the existing error_message, logger.error and raising behavior.

---

Outside diff comments:
In `@libs/framework-core/source/ftrack_framework_core/asset/asset_info.py`:
- Around line 124-129: The code in _check_asset_info_dependencies uses
eval(dependency) which is unsafe; replace it with a safe parser such as
ast.literal_eval (import ast) or json.loads depending on the expected input,
e.g. parse the dependency string into a dict via ast.literal_eval and then pass
that dict to FtrackAssetInfo (retain the existing variables dict_dept and
dep_asset_info). Also add minimal exception handling around the parse to catch
ValueError/SyntaxError (log/raise as appropriate) so malformed strings don't
silently crash.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 7a71b87d-5109-43cd-a140-c8450e2e92f9

📥 Commits

Reviewing files that changed from the base of the PR and between 843bde9 and 9a12620.

📒 Files selected for processing (4)
  • libs/framework-core/source/ftrack_framework_core/asset/asset_info.py
  • libs/framework-core/source/ftrack_framework_core/client/__init__.py
  • libs/utils/source/ftrack_utils/string/__init__.py
  • projects/nuke-studio/source/ftrack_nuke_studio/exception.py

Comment on lines 63 to +65
if not asset_info_options:
self.logger.error("asset_info_options is empty")
if six.PY2:
return json.loads(base64.b64decode(asset_info_options))
else:
return json.loads(
base64.b64decode(asset_info_options).decode('ascii')
)
return json.loads(base64.b64decode(asset_info_options).decode("ascii"))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

decode_options logs an error on empty input but then crashes anyway.

When asset_info_options is empty, the error is logged and execution continues into base64.b64decode("")""json.loads(""), which raises json.JSONDecodeError. The log message is misleading because the call still fails with an unrelated exception. Return early (or a safe default) after logging.

🛠️ Proposed fix
         if not asset_info_options:
             self.logger.error("asset_info_options is empty")
+            return None
         return json.loads(base64.b64decode(asset_info_options).decode("ascii"))
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if not asset_info_options:
self.logger.error("asset_info_options is empty")
if six.PY2:
return json.loads(base64.b64decode(asset_info_options))
else:
return json.loads(
base64.b64decode(asset_info_options).decode('ascii')
)
return json.loads(base64.b64decode(asset_info_options).decode("ascii"))
if not asset_info_options:
self.logger.error("asset_info_options is empty")
return None
return json.loads(base64.b64decode(asset_info_options).decode("ascii"))
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@libs/framework-core/source/ftrack_framework_core/asset/asset_info.py` around
lines 63 - 65, The decode_options path logs an error when asset_info_options is
empty but then continues and causes json.JSONDecodeError; update the method (the
block handling asset_info_options in decode_options) to return early with a safe
default (e.g., {} or None) immediately after logging the error so it doesn't
call base64.b64decode/json.loads on an empty string; ensure the change
references the asset_info_options variable and uses self.logger.error in the
same method to preserve the existing log behavior.

for config in sublist
]
for tool_config_name in options.get('tool_configs'):
for tool_config_name in options.get("tool_configs"):

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Handle missing tool_configs before iterating.

At Line 385, options.get("tool_configs") can be None, which causes a TypeError when iterated.

Suggested fix
-            for tool_config_name in options.get("tool_configs"):
+            for tool_config_name in options.get("tool_configs", []):
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
for tool_config_name in options.get("tool_configs"):
for tool_config_name in options.get("tool_configs", []):
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@libs/framework-core/source/ftrack_framework_core/client/__init__.py` at line
385, The loop over options.get("tool_configs") can raise TypeError when
tool_configs is None; update the iterator in the block that contains the line
'for tool_config_name in options.get("tool_configs")' to guard against None
(e.g., use options.get("tool_configs", []) or check and set tool_configs =
options.get("tool_configs") or [] before the loop) so the code safely handles
missing or null tool_configs values.

Comment on lines 426 to 435
if dialog_class:
if not isinstance(dialog_class, FrameworkDialog):
error_message = (
'The provided class {} is not instance of the base framework '
'widget. Please provide a supported widget.'.format(
"The provided class {} is not instance of the base framework "
"widget. Please provide a supported widget.".format(
dialog_class
)
)
self.logger.error(error_message)
raise Exception(error_message)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify how run_dialog is called and whether dialog_class is passed as a class.
rg -nP --type=py -C2 '\brun_dialog\s*\('
rg -nP --type=py -C2 'dialog_class\s*='

Repository: ftrackhq/integrations

Length of output: 3311


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate FrameworkDialog definition
rg -n --type=py "class\s+FrameworkDialog|FrameworkDialog\s*=" libs/framework-core/source/ftrack_framework_core

# Inspect run_dialog implementation around the validation and usage of dialog_class
sed -n '390,520p' libs/framework-core/source/ftrack_framework_core/client/__init__.py

# Find how registry.dialogs stores dialog entries and specifically what "extension" is
rg -n --type=py "registry\.dialogs|dialogs\s*=" libs/framework-core/source/ftrack_framework_core/client/__init__.py libs/framework-core/source/ftrack_framework_core -S
rg -n --type=py "\"extension\"\s*:" libs/framework-core/source/ftrack_framework_core/client/__init__.py libs/framework-core/source/ftrack_framework_core -S
sed -n '520,700p' libs/framework-core/source/ftrack_framework_core/client/__init__.py

Repository: ftrackhq/integrations

Length of output: 8269


🏁 Script executed:

#!/bin/bash
set -euo pipefail
# Inspect registry.add() and how dialogs are stored
sed -n '1,240p' libs/framework-core/source/ftrack_framework_core/registry/__init__.py
rg -n --type=py "def add\(" libs/framework-core/source/ftrack_framework_core/registry/__init__.py
rg -n --type=py "dialogs\s*=" libs/framework-core/source/ftrack_framework_core/registry/__init__.py
rg -n --type=py "extension_type=\"dialog\"|extension\"?\s*:\s*extension" libs/framework-core/source/ftrack_framework_core/registry/__init__.py
sed -n '240,420p' libs/framework-core/source/ftrack_framework_core/registry/__init__.py

Repository: ftrackhq/integrations

Length of output: 7402


🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n --type=py "run_dialog\s*\(.*dialog_class\s*=" .
rg -n --type=py "dialog_class\s*=" libs/framework-core/source/ftrack_framework_core
rg -n --type=py "add\(\s*extension_type\s*=\s*\"dialog\"|extension_type\s*=\s*'dialog'" -S .

Repository: ftrackhq/integrations

Length of output: 47


🏁 Script executed:

#!/bin/bash
set -euo pipefail
# Find any call sites or usage of dialog_class across the repo
rg -n --type=py "\bdialog_class\b" .
rg -n --type=py "run_dialog\s*\(" libs/framework-core/source/ftrack_framework_core tests tools . || true

Repository: ftrackhq/integrations

Length of output: 2508


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect cookiecutter template call site
sed -n '1,120p' "tools/cookiecutter-framework-project/{{cookiecutter.project_name}}/source/{{ cookiecutter.package_name }}/__init__.py"

# Inspect standalone UI manual test call site
sed -n '1,140p' tests/framework/manual/standalone_ui_test.py

Repository: ftrackhq/integrations

Length of output: 5920


🏁 Script executed:

#!/bin/bash
set -euo pipefail
# Find where dialogs are added to the registry
rg -n --type=py "extension_type\s*=\s*['\"]dialog['\"]" .
rg -n --type=py "\badd\(\s*extension_type\s*=\s*['\"]dialog['\"]" .
# Find dialog classes exported/declared as framework widgets
rg -n --type=py "class\s+\w*Dialog\b" libs/framework-core/source/ftrack_framework_core/widget/dialog.py libs/framework-core/source/ftrack_framework_core/widget .
rg -n --type=py "FrameworkDialog" . -S

Repository: ftrackhq/integrations

Length of output: 184


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect FrameworkDialog base class details
sed -n '1,220p' libs/framework-core/source/ftrack_framework_core/widget/dialog.py

# Find any concrete dialog implementations inheriting FrameworkDialog
rg -n --type=py "class\s+\w*Dialog\w*\(.*FrameworkDialog" libs/framework-core/source/ftrack_framework_core || true
rg -n --type=py "FrameworkDialog" libs/framework-core/source/ftrack_framework_core/widget || true

# Search for any extensions that declare dialog widgets (likely in a 'widget' or 'dialog' module)
rg -n --type=py "extension_type\s*=\s*['\"]dialog['\"]" . || true
rg -n --type=py "extension_type\s*:\s*['\"]dialog['\"]" . || true
rg -n --type=py "\"dialog\"" libs/framework-core/source/ftrack_framework_core -S || true

Repository: ftrackhq/integrations

Length of output: 7390


Fix dialog_class validation to accept dialog widget subclasses

Client.run_dialog() later instantiates dialog_class via dialog = dialog_class(...) and stores/compares it as the registry "extension". The current isinstance(dialog_class, FrameworkDialog) check rejects valid dialog classes.

File: libs/framework-core/source/ftrack_framework_core/client/__init__.py
Lines: 426-435

        if dialog_class:
            if not isinstance(dialog_class, FrameworkDialog):
                error_message = (
                    "The provided class {} is not instance of the base framework "
                    "widget. Please provide a supported widget.".format(
                        dialog_class
                    )
                )
                self.logger.error(error_message)
                raise Exception(error_message)
Suggested fix
-            if not isinstance(dialog_class, FrameworkDialog):
+            if not (
+                isinstance(dialog_class, type)
+                and issubclass(dialog_class, FrameworkDialog)
+            ):
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@libs/framework-core/source/ftrack_framework_core/client/__init__.py` around
lines 426 - 435, The validation incorrectly uses isinstance(dialog_class,
FrameworkDialog) which rejects valid dialog classes; update the check in
Client.run_dialog (the dialog_class validation block) to verify that
dialog_class is a class and a subclass of FrameworkDialog (e.g., use
inspect.isclass(dialog_class) and issubclass(dialog_class, FrameworkDialog)) and
log/raise the same error if that test fails; ensure you import inspect if needed
and preserve the existing error_message, logger.error and raising behavior.

self.dialog = dialog
# If a docking function is provided, use it
if dialog_options.get('docked') and dock_func:
if dialog_options.get("docked") and dock_func:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Guard dialog_options before calling .get().

At Line 478, dialog_options defaults to None, so dialog_options.get(...) can raise AttributeError.

Suggested fix
-        if dialog_options.get("docked") and dock_func:
+        if dialog_options and dialog_options.get("docked") and dock_func:
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if dialog_options.get("docked") and dock_func:
if dialog_options and dialog_options.get("docked") and dock_func:
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@libs/framework-core/source/ftrack_framework_core/client/__init__.py` at line
478, The condition uses dialog_options.get("docked") but dialog_options can be
None; update the check to guard for None before calling .get(), for example
change the conditional around the existing logic to ensure dialog_options is
truthy (e.g. if dialog_options and dialog_options.get("docked") and dock_func)
or use a safe fallback (e.g. if (dialog_options or {}).get("docked") and
dock_func) so dialog_options.get is never invoked on None; modify the
conditional that currently reads `if dialog_options.get("docked") and
dock_func:` accordingly.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant