From 72ebbb98e8023ad2f80ddcdf53ea7e829eeb2bbc Mon Sep 17 00:00:00 2001 From: davidnichols-ops Date: Sat, 13 Jun 2026 00:09:58 -0500 Subject: [PATCH 1/3] feat: add option to preserve class ordering during model deployment Add DISABLE_CLASS_SORTING configuration option to prevent automatic alphabetical sorting of class names during YOLO model deployment. Users can set ROBOFLOW_DISABLE_CLASS_SORTING=true to maintain custom class ordering from model checkpoints. - Add DISABLE_CLASS_SORTING to config.py (defaults to false) - Update model_processor.py to conditionally apply sorting - Include warning comment about user responsibility for proper class ordering - Maintain backwards compatibility with default behavior Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- roboflow/config.py | 1 + roboflow/util/model_processor.py | 7 ++++++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/roboflow/config.py b/roboflow/config.py index 0d569e95..e975d38c 100644 --- a/roboflow/config.py +++ b/roboflow/config.py @@ -86,6 +86,7 @@ def get_conditional_configuration_variable(key, default): RF_WORKSPACES = get_conditional_configuration_variable("workspaces", default={}) TQDM_DISABLE = os.getenv("TQDM_DISABLE", None) +DISABLE_CLASS_SORTING = os.getenv("ROBOFLOW_DISABLE_CLASS_SORTING", "false").lower() == "true" def load_roboflow_api_key(workspace_url=None): diff --git a/roboflow/util/model_processor.py b/roboflow/util/model_processor.py index 0022c11a..8adcb5f4 100644 --- a/roboflow/util/model_processor.py +++ b/roboflow/util/model_processor.py @@ -7,6 +7,7 @@ import yaml from roboflow.config import ( + DISABLE_CLASS_SORTING, TASK_CLS, TASK_DET, TASK_OBB, @@ -224,7 +225,11 @@ def _process_yolo(model_type: str, model_path: str, filename: str) -> tuple[str, class_names = [] for i, val in enumerate(model_instance.names): class_names.append((val, model_instance.names[val])) - class_names.sort(key=lambda x: x[0]) + # NOTE: When DISABLE_CLASS_SORTING is enabled, users are responsible for ensuring + # their model's names dict has properly ordered/sequential keys. Non-sequential keys + # may result in incorrect class-to-index mappings. + if not DISABLE_CLASS_SORTING: + class_names.sort(key=lambda x: x[0]) class_names = [x[1] for x in class_names] if ( From b86e7256d1e7056252dc54ea799e4f0f01031fd2 Mon Sep 17 00:00:00 2001 From: davidnichols-ops Date: Sat, 13 Jun 2026 00:37:29 -0500 Subject: [PATCH 2/3] test: add tests and changelog entry for class sorting feature - Add test suite for DISABLE_CLASS_SORTING configuration - Test default value, environment variable parsing, and case insensitivity - Update CHANGELOG.md with new feature entry Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- CHANGELOG.md | 2 + tests/util/test_class_sorting.py | 65 ++++++++++++++++++++++++++++++++ 2 files changed, 67 insertions(+) create mode 100644 tests/util/test_class_sorting.py diff --git a/CHANGELOG.md b/CHANGELOG.md index b1c1fd19..35ff89f2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,8 @@ All notable changes to this project will be documented in this file. - Weight upload support for yolo26-sem semantic segmentation models via `version.deploy()` and `workspace.deploy_model()` +- `ROBOFLOW_DISABLE_CLASS_SORTING` environment variable to preserve custom + class ordering during YOLO model deployment (opt-in, defaults to false) ## 1.3.9 diff --git a/tests/util/test_class_sorting.py b/tests/util/test_class_sorting.py new file mode 100644 index 00000000..cd06e573 --- /dev/null +++ b/tests/util/test_class_sorting.py @@ -0,0 +1,65 @@ +import os +import sys +import unittest +from unittest.mock import patch +import importlib.util + +# Get the path to config.py directly +config_path = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), 'roboflow', 'config.py') + + +class TestClassSorting(unittest.TestCase): + """Test DISABLE_CLASS_SORTING configuration for class ordering.""" + + def test_disable_class_sorting_default_value(self): + """Test that DISABLE_CLASS_SORTING defaults to False.""" + # Load config module directly without triggering __init__.py + spec = importlib.util.spec_from_file_location("config", config_path) + config = importlib.util.module_from_spec(spec) + spec.loader.exec_module(config) + + self.assertFalse(config.DISABLE_CLASS_SORTING) + + def test_disable_class_sorting_env_var_true(self): + """Test that ROBOFLOW_DISABLE_CLASS_SORTING=true sets config to True.""" + with patch.dict(os.environ, {"ROBOFLOW_DISABLE_CLASS_SORTING": "true"}): + # Reload config to pick up env var + spec = importlib.util.spec_from_file_location("config", config_path) + config = importlib.util.module_from_spec(spec) + spec.loader.exec_module(config) + + # After reload, should be True + self.assertTrue(config.DISABLE_CLASS_SORTING) + + def test_disable_class_sorting_env_var_false(self): + """Test that ROBOFLOW_DISABLE_CLASS_SORTING=false keeps config as False.""" + with patch.dict(os.environ, {"ROBOFLOW_DISABLE_CLASS_SORTING": "false"}): + spec = importlib.util.spec_from_file_location("config", config_path) + config = importlib.util.module_from_spec(spec) + spec.loader.exec_module(config) + + self.assertFalse(config.DISABLE_CLASS_SORTING) + + def test_disable_class_sorting_env_var_case_insensitive(self): + """Test that env var is case-insensitive.""" + for value in ["True", "TRUE", "tRuE"]: + with patch.dict(os.environ, {"ROBOFLOW_DISABLE_CLASS_SORTING": value}): + spec = importlib.util.spec_from_file_location("config", config_path) + config = importlib.util.module_from_spec(spec) + spec.loader.exec_module(config) + + self.assertTrue(config.DISABLE_CLASS_SORTING) + + def test_config_import(self): + """Test that DISABLE_CLASS_SORTING exists and is a boolean.""" + spec = importlib.util.spec_from_file_location("config", config_path) + config = importlib.util.module_from_spec(spec) + spec.loader.exec_module(config) + + config_value = config.DISABLE_CLASS_SORTING + self.assertIsNotNone(config_value) + self.assertIsInstance(config_value, bool) + + +if __name__ == "__main__": + unittest.main() From 1ac9429484128959ebbe9fbdb2b37199a4d901cf Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sat, 13 Jun 2026 05:37:47 +0000 Subject: [PATCH 3/3] =?UTF-8?q?fix(pre=5Fcommit):=20=F0=9F=8E=A8=20auto=20?= =?UTF-8?q?format=20pre-commit=20hooks?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/util/test_class_sorting.py | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/tests/util/test_class_sorting.py b/tests/util/test_class_sorting.py index cd06e573..ad4e930f 100644 --- a/tests/util/test_class_sorting.py +++ b/tests/util/test_class_sorting.py @@ -1,11 +1,10 @@ +import importlib.util import os -import sys import unittest from unittest.mock import patch -import importlib.util # Get the path to config.py directly -config_path = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), 'roboflow', 'config.py') +config_path = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), "roboflow", "config.py") class TestClassSorting(unittest.TestCase): @@ -17,7 +16,7 @@ def test_disable_class_sorting_default_value(self): spec = importlib.util.spec_from_file_location("config", config_path) config = importlib.util.module_from_spec(spec) spec.loader.exec_module(config) - + self.assertFalse(config.DISABLE_CLASS_SORTING) def test_disable_class_sorting_env_var_true(self): @@ -27,7 +26,7 @@ def test_disable_class_sorting_env_var_true(self): spec = importlib.util.spec_from_file_location("config", config_path) config = importlib.util.module_from_spec(spec) spec.loader.exec_module(config) - + # After reload, should be True self.assertTrue(config.DISABLE_CLASS_SORTING) @@ -37,7 +36,7 @@ def test_disable_class_sorting_env_var_false(self): spec = importlib.util.spec_from_file_location("config", config_path) config = importlib.util.module_from_spec(spec) spec.loader.exec_module(config) - + self.assertFalse(config.DISABLE_CLASS_SORTING) def test_disable_class_sorting_env_var_case_insensitive(self): @@ -47,7 +46,7 @@ def test_disable_class_sorting_env_var_case_insensitive(self): spec = importlib.util.spec_from_file_location("config", config_path) config = importlib.util.module_from_spec(spec) spec.loader.exec_module(config) - + self.assertTrue(config.DISABLE_CLASS_SORTING) def test_config_import(self): @@ -55,7 +54,7 @@ def test_config_import(self): spec = importlib.util.spec_from_file_location("config", config_path) config = importlib.util.module_from_spec(spec) spec.loader.exec_module(config) - + config_value = config.DISABLE_CLASS_SORTING self.assertIsNotNone(config_value) self.assertIsInstance(config_value, bool)