diff --git a/.asv/results/benchmarks.json b/.asv/results/benchmarks.json new file mode 100644 index 00000000..98f9eb4f --- /dev/null +++ b/.asv/results/benchmarks.json @@ -0,0 +1,385 @@ +{ + "Components vs Django.peakmem_render_lg_first": { + "code": "class DjangoComponentsVsDjangoTests:\n @benchmark(\n pretty_name=\"render - large - first render (mem)\",\n group_name=DJC_VS_DJ_GROUP,\n number=1,\n rounds=5,\n params={\n \"renderer\": [\"django\", \"django-components\"],\n },\n setup=lambda renderer: setup_templating_memory_benchmark(renderer, \"lg\", \"first\", \"isolated\"),\n )\n def peakmem_render_lg_first(self, renderer: TemplatingRenderer):\n do_render()\n\nsetup=lambda renderer: setup_templating_memory_benchmark(renderer, \"lg\", \"first\", \"isolated\"),", + "name": "Components vs Django.peakmem_render_lg_first", + "param_names": [ + "renderer" + ], + "params": [ + [ + "'django'", + "'django-components'" + ] + ], + "pretty_name": "render - large - first render (mem)", + "type": "peakmemory", + "unit": "bytes", + "version": "301c396f017f45a5b3f71e85df58d15f54153fcfd951af7ef424641d4b31b528" + }, + "Components vs Django.peakmem_render_lg_subsequent": { + "code": "class DjangoComponentsVsDjangoTests:\n @benchmark(\n pretty_name=\"render - large - second render (mem)\",\n group_name=DJC_VS_DJ_GROUP,\n number=1,\n rounds=5,\n params={\n \"renderer\": [\"django\", \"django-components\"],\n },\n setup=lambda renderer: setup_templating_memory_benchmark(renderer, \"lg\", \"subsequent\", \"isolated\"),\n )\n def peakmem_render_lg_subsequent(self, renderer: TemplatingRenderer):\n do_render()\n\nsetup=lambda renderer: setup_templating_memory_benchmark(renderer, \"lg\", \"subsequent\", \"isolated\"),", + "name": "Components vs Django.peakmem_render_lg_subsequent", + "param_names": [ + "renderer" + ], + "params": [ + [ + "'django'", + "'django-components'" + ] + ], + "pretty_name": "render - large - second render (mem)", + "type": "peakmemory", + "unit": "bytes", + "version": "9a44e9999ef3ef42ea7e01323727490244febb43d66a87a4d8f88c6b8a133b8b" + }, + "Components vs Django.peakmem_render_sm_first": { + "code": "class DjangoComponentsVsDjangoTests:\n @benchmark(\n pretty_name=\"render - small - first render (mem)\",\n group_name=DJC_VS_DJ_GROUP,\n number=1,\n rounds=5,\n params={\n \"renderer\": [\"django\", \"django-components\"],\n },\n setup=lambda renderer: setup_templating_memory_benchmark(renderer, \"sm\", \"first\", \"isolated\"),\n )\n def peakmem_render_sm_first(self, renderer: TemplatingRenderer):\n do_render()\n\nsetup=lambda renderer: setup_templating_memory_benchmark(renderer, \"sm\", \"first\", \"isolated\"),", + "name": "Components vs Django.peakmem_render_sm_first", + "param_names": [ + "renderer" + ], + "params": [ + [ + "'django'", + "'django-components'" + ] + ], + "pretty_name": "render - small - first render (mem)", + "type": "peakmemory", + "unit": "bytes", + "version": "e93b7a5193681c883edf85bdb30b1bc0821263bf51033fdcee215b155085e036" + }, + "Components vs Django.peakmem_render_sm_subsequent": { + "code": "class DjangoComponentsVsDjangoTests:\n @benchmark(\n pretty_name=\"render - small - second render (mem)\",\n group_name=DJC_VS_DJ_GROUP,\n number=1,\n rounds=5,\n params={\n \"renderer\": [\"django\", \"django-components\"],\n },\n setup=lambda renderer: setup_templating_memory_benchmark(renderer, \"sm\", \"subsequent\", \"isolated\"),\n )\n def peakmem_render_sm_subsequent(self, renderer: TemplatingRenderer):\n do_render()\n\nsetup=lambda renderer: setup_templating_memory_benchmark(renderer, \"sm\", \"subsequent\", \"isolated\"),", + "name": "Components vs Django.peakmem_render_sm_subsequent", + "param_names": [ + "renderer" + ], + "params": [ + [ + "'django'", + "'django-components'" + ] + ], + "pretty_name": "render - small - second render (mem)", + "type": "peakmemory", + "unit": "bytes", + "version": "b46e0820b18950aa7cc5e61306ff3425b76b4da9dca42d64fae5b1d25c6c9026" + }, + "Components vs Django.timeraw_render_lg_first": { + "code": "class DjangoComponentsVsDjangoTests:\n @benchmark(\n pretty_name=\"render - large - first render\",\n group_name=DJC_VS_DJ_GROUP,\n number=1,\n rounds=5,\n params={\n \"renderer\": [\"django\", \"django-components\"],\n },\n include_in_quick_benchmark=True,\n )\n def timeraw_render_lg_first(self, renderer: TemplatingRenderer):\n return prepare_templating_benchmark(renderer, \"lg\", \"first\", \"isolated\")", + "min_run_count": 2, + "name": "Components vs Django.timeraw_render_lg_first", + "number": 1, + "param_names": [ + "renderer" + ], + "params": [ + [ + "'django'", + "'django-components'" + ] + ], + "pretty_name": "render - large - first render", + "repeat": 0, + "rounds": 5, + "sample_time": 0.01, + "type": "time", + "unit": "seconds", + "version": "be3bf6236960046a028b6ea007aad28b2337fc2b906b8ce317a09a5d4f1a6193", + "warmup_time": -1 + }, + "Components vs Django.timeraw_render_lg_subsequent": { + "code": "class DjangoComponentsVsDjangoTests:\n @benchmark(\n pretty_name=\"render - large - second render\",\n group_name=DJC_VS_DJ_GROUP,\n number=1,\n rounds=5,\n params={\n \"renderer\": [\"django\", \"django-components\"],\n },\n )\n def timeraw_render_lg_subsequent(self, renderer: TemplatingRenderer):\n return prepare_templating_benchmark(renderer, \"lg\", \"subsequent\", \"isolated\")", + "min_run_count": 2, + "name": "Components vs Django.timeraw_render_lg_subsequent", + "number": 1, + "param_names": [ + "renderer" + ], + "params": [ + [ + "'django'", + "'django-components'" + ] + ], + "pretty_name": "render - large - second render", + "repeat": 0, + "rounds": 5, + "sample_time": 0.01, + "type": "time", + "unit": "seconds", + "version": "b98221c11a0ee6e9de0778d416d31b9dd514a674d9017a2bb9b2fc1cd0f01920", + "warmup_time": -1 + }, + "Components vs Django.timeraw_render_sm_first": { + "code": "class DjangoComponentsVsDjangoTests:\n @benchmark(\n pretty_name=\"render - small - first render\",\n group_name=DJC_VS_DJ_GROUP,\n number=1,\n rounds=5,\n params={\n \"renderer\": [\"django\", \"django-components\"],\n },\n )\n def timeraw_render_sm_first(self, renderer: TemplatingRenderer):\n return prepare_templating_benchmark(renderer, \"sm\", \"first\", \"isolated\")", + "min_run_count": 2, + "name": "Components vs Django.timeraw_render_sm_first", + "number": 1, + "param_names": [ + "renderer" + ], + "params": [ + [ + "'django'", + "'django-components'" + ] + ], + "pretty_name": "render - small - first render", + "repeat": 0, + "rounds": 5, + "sample_time": 0.01, + "type": "time", + "unit": "seconds", + "version": "f1fc17e4a31c71f4d9265f1122da52e7cf57addb4dfa02606e303b33d6431b9b", + "warmup_time": -1 + }, + "Components vs Django.timeraw_render_sm_subsequent": { + "code": "class DjangoComponentsVsDjangoTests:\n @benchmark(\n pretty_name=\"render - small - second render\",\n group_name=DJC_VS_DJ_GROUP,\n number=1,\n rounds=5,\n params={\n \"renderer\": [\"django\", \"django-components\"],\n },\n )\n def timeraw_render_sm_subsequent(self, renderer: TemplatingRenderer):\n return prepare_templating_benchmark(renderer, \"sm\", \"subsequent\", \"isolated\")", + "min_run_count": 2, + "name": "Components vs Django.timeraw_render_sm_subsequent", + "number": 1, + "param_names": [ + "renderer" + ], + "params": [ + [ + "'django'", + "'django-components'" + ] + ], + "pretty_name": "render - small - second render", + "repeat": 0, + "rounds": 5, + "sample_time": 0.01, + "type": "time", + "unit": "seconds", + "version": "6fce1cd85a9344fee383b40a22f27862120b9488a628420625592dc14e0307d3", + "warmup_time": -1 + }, + "Components vs Django.timeraw_startup_lg": { + "code": "class DjangoComponentsVsDjangoTests:\n @benchmark(\n pretty_name=\"startup - large\",\n group_name=DJC_VS_DJ_GROUP,\n number=1,\n rounds=5,\n params={\n \"renderer\": [\"django\", \"django-components\"],\n },\n )\n def timeraw_startup_lg(self, renderer: TemplatingRenderer):\n return prepare_templating_benchmark(renderer, \"lg\", \"startup\", \"isolated\")", + "min_run_count": 2, + "name": "Components vs Django.timeraw_startup_lg", + "number": 1, + "param_names": [ + "renderer" + ], + "params": [ + [ + "'django'", + "'django-components'" + ] + ], + "pretty_name": "startup - large", + "repeat": 0, + "rounds": 5, + "sample_time": 0.01, + "type": "time", + "unit": "seconds", + "version": "53151821c128ad0ecfb0707fff3146e1abd8d0bcfa301aa056b5d3fae3d793e2", + "warmup_time": -1 + }, + "Other.timeraw_import_time": { + "code": "class OtherTests:\n @benchmark(\n pretty_name=\"import time\",\n group_name=OTHER_GROUP,\n number=1,\n rounds=5,\n )\n def timeraw_import_time(self):\n return prepare_templating_benchmark(\"django-components\", \"lg\", \"startup\", \"isolated\", imports_only=True)", + "min_run_count": 2, + "name": "Other.timeraw_import_time", + "number": 1, + "param_names": [], + "params": [], + "pretty_name": "import time", + "repeat": 0, + "rounds": 5, + "sample_time": 0.01, + "type": "time", + "unit": "seconds", + "version": "a0a1c1c0db22509410b946d0d4384b52ea4a09b47b6048d7d1cfb89b0c7fe5c3", + "warmup_time": -1 + }, + "isolated vs django modes.peakmem_render_lg_first": { + "code": "class IsolatedVsDjangoContextModesTests:\n @benchmark(\n pretty_name=\"render - large - first render (mem)\",\n group_name=DJC_ISOLATED_VS_NON_GROUP,\n number=1,\n rounds=5,\n params={\n \"context_mode\": [\"isolated\", \"django\"],\n },\n setup=lambda context_mode: setup_templating_memory_benchmark(\n \"django-components\",\n \"lg\",\n \"first\",\n context_mode,\n ),\n )\n def peakmem_render_lg_first(self, context_mode: DjcContextMode):\n do_render()\n\nsetup=lambda context_mode: setup_templating_memory_benchmark(\n \"django-components\",\n \"lg\",\n \"first\",\n context_mode,\n),", + "name": "isolated vs django modes.peakmem_render_lg_first", + "param_names": [ + "context_mode" + ], + "params": [ + [ + "'isolated'", + "'django'" + ] + ], + "pretty_name": "render - large - first render (mem)", + "type": "peakmemory", + "unit": "bytes", + "version": "c4bf0016d48d210f08b8db733b57c7dcba1cebbf548c458b93b86ace387067e9" + }, + "isolated vs django modes.peakmem_render_lg_subsequent": { + "code": "class IsolatedVsDjangoContextModesTests:\n @benchmark(\n pretty_name=\"render - large - second render (mem)\",\n group_name=DJC_ISOLATED_VS_NON_GROUP,\n number=1,\n rounds=5,\n params={\n \"context_mode\": [\"isolated\", \"django\"],\n },\n setup=lambda context_mode: setup_templating_memory_benchmark(\n \"django-components\",\n \"lg\",\n \"subsequent\",\n context_mode,\n ),\n )\n def peakmem_render_lg_subsequent(self, context_mode: DjcContextMode):\n do_render()\n\nsetup=lambda context_mode: setup_templating_memory_benchmark(\n \"django-components\",\n \"lg\",\n \"subsequent\",\n context_mode,\n),", + "name": "isolated vs django modes.peakmem_render_lg_subsequent", + "param_names": [ + "context_mode" + ], + "params": [ + [ + "'isolated'", + "'django'" + ] + ], + "pretty_name": "render - large - second render (mem)", + "type": "peakmemory", + "unit": "bytes", + "version": "65bb1b8586487197a79bb6073e4c71642877b845b6eb42d1bd32398299daffbf" + }, + "isolated vs django modes.peakmem_render_sm_first": { + "code": "class IsolatedVsDjangoContextModesTests:\n @benchmark(\n pretty_name=\"render - small - first render (mem)\",\n group_name=DJC_ISOLATED_VS_NON_GROUP,\n number=1,\n rounds=5,\n params={\n \"context_mode\": [\"isolated\", \"django\"],\n },\n setup=lambda context_mode: setup_templating_memory_benchmark(\"django-components\", \"sm\", \"first\", context_mode),\n )\n def peakmem_render_sm_first(self, context_mode: DjcContextMode):\n do_render()\n\nsetup=lambda context_mode: setup_templating_memory_benchmark(\"django-components\", \"sm\", \"first\", context_mode),", + "name": "isolated vs django modes.peakmem_render_sm_first", + "param_names": [ + "context_mode" + ], + "params": [ + [ + "'isolated'", + "'django'" + ] + ], + "pretty_name": "render - small - first render (mem)", + "type": "peakmemory", + "unit": "bytes", + "version": "c51b91fc583295776062822225e720b5ed71aef9c9288217c401c54283c62840" + }, + "isolated vs django modes.peakmem_render_sm_subsequent": { + "code": "class IsolatedVsDjangoContextModesTests:\n @benchmark(\n pretty_name=\"render - small - second render (mem)\",\n group_name=DJC_ISOLATED_VS_NON_GROUP,\n number=1,\n rounds=5,\n params={\n \"context_mode\": [\"isolated\", \"django\"],\n },\n setup=lambda context_mode: setup_templating_memory_benchmark(\n \"django-components\",\n \"sm\",\n \"subsequent\",\n context_mode,\n ),\n )\n def peakmem_render_sm_subsequent(self, context_mode: DjcContextMode):\n do_render()\n\nsetup=lambda context_mode: setup_templating_memory_benchmark(\n \"django-components\",\n \"sm\",\n \"subsequent\",\n context_mode,\n),", + "name": "isolated vs django modes.peakmem_render_sm_subsequent", + "param_names": [ + "context_mode" + ], + "params": [ + [ + "'isolated'", + "'django'" + ] + ], + "pretty_name": "render - small - second render (mem)", + "type": "peakmemory", + "unit": "bytes", + "version": "54d747fb8f40179b7ff3d2fc49eb195909ad1c880b5ef7b82f82742b27b67260" + }, + "isolated vs django modes.timeraw_render_lg_first": { + "code": "class IsolatedVsDjangoContextModesTests:\n @benchmark(\n pretty_name=\"render - large - first render\",\n group_name=DJC_ISOLATED_VS_NON_GROUP,\n number=1,\n rounds=5,\n params={\n \"context_mode\": [\"isolated\", \"django\"],\n },\n )\n def timeraw_render_lg_first(self, context_mode: DjcContextMode):\n return prepare_templating_benchmark(\"django-components\", \"lg\", \"first\", context_mode)", + "min_run_count": 2, + "name": "isolated vs django modes.timeraw_render_lg_first", + "number": 1, + "param_names": [ + "context_mode" + ], + "params": [ + [ + "'isolated'", + "'django'" + ] + ], + "pretty_name": "render - large - first render", + "repeat": 0, + "rounds": 5, + "sample_time": 0.01, + "type": "time", + "unit": "seconds", + "version": "f94af83427c6346f88f8785a3cd2fc42415ac5a9fbbdb7de71d27e22e6a81699", + "warmup_time": -1 + }, + "isolated vs django modes.timeraw_render_lg_subsequent": { + "code": "class IsolatedVsDjangoContextModesTests:\n @benchmark(\n pretty_name=\"render - large - second render\",\n group_name=DJC_ISOLATED_VS_NON_GROUP,\n number=1,\n rounds=5,\n params={\n \"context_mode\": [\"isolated\", \"django\"],\n },\n )\n def timeraw_render_lg_subsequent(self, context_mode: DjcContextMode):\n return prepare_templating_benchmark(\"django-components\", \"lg\", \"subsequent\", context_mode)", + "min_run_count": 2, + "name": "isolated vs django modes.timeraw_render_lg_subsequent", + "number": 1, + "param_names": [ + "context_mode" + ], + "params": [ + [ + "'isolated'", + "'django'" + ] + ], + "pretty_name": "render - large - second render", + "repeat": 0, + "rounds": 5, + "sample_time": 0.01, + "type": "time", + "unit": "seconds", + "version": "9f7c2fde6b33f0451a1794ed903c48d96cd7822f67da502cec36fe8e977c2414", + "warmup_time": -1 + }, + "isolated vs django modes.timeraw_render_sm_first": { + "code": "class IsolatedVsDjangoContextModesTests:\n @benchmark(\n pretty_name=\"render - small - first render\",\n group_name=DJC_ISOLATED_VS_NON_GROUP,\n number=1,\n rounds=5,\n params={\n \"context_mode\": [\"isolated\", \"django\"],\n },\n )\n def timeraw_render_sm_first(self, context_mode: DjcContextMode):\n return prepare_templating_benchmark(\"django-components\", \"sm\", \"first\", context_mode)", + "min_run_count": 2, + "name": "isolated vs django modes.timeraw_render_sm_first", + "number": 1, + "param_names": [ + "context_mode" + ], + "params": [ + [ + "'isolated'", + "'django'" + ] + ], + "pretty_name": "render - small - first render", + "repeat": 0, + "rounds": 5, + "sample_time": 0.01, + "type": "time", + "unit": "seconds", + "version": "d15ca68909d7f1f43ff16863befb6f42681f17461417fc0069eefd6db3569296", + "warmup_time": -1 + }, + "isolated vs django modes.timeraw_render_sm_subsequent": { + "code": "class IsolatedVsDjangoContextModesTests:\n @benchmark(\n pretty_name=\"render - small - second render\",\n group_name=DJC_ISOLATED_VS_NON_GROUP,\n number=1,\n rounds=5,\n params={\n \"context_mode\": [\"isolated\", \"django\"],\n },\n )\n def timeraw_render_sm_subsequent(self, context_mode: DjcContextMode):\n return prepare_templating_benchmark(\"django-components\", \"sm\", \"subsequent\", context_mode)", + "min_run_count": 2, + "name": "isolated vs django modes.timeraw_render_sm_subsequent", + "number": 1, + "param_names": [ + "context_mode" + ], + "params": [ + [ + "'isolated'", + "'django'" + ] + ], + "pretty_name": "render - small - second render", + "repeat": 0, + "rounds": 5, + "sample_time": 0.01, + "type": "time", + "unit": "seconds", + "version": "7444bc9516dd087e3f420349345eae991ad6941bbd22fce45265b18034b7cf77", + "warmup_time": -1 + }, + "isolated vs django modes.timeraw_startup_lg": { + "code": "class IsolatedVsDjangoContextModesTests:\n @benchmark(\n pretty_name=\"startup - large\",\n group_name=DJC_ISOLATED_VS_NON_GROUP,\n number=1,\n rounds=5,\n params={\n \"context_mode\": [\"isolated\", \"django\"],\n },\n )\n def timeraw_startup_lg(self, context_mode: DjcContextMode):\n return prepare_templating_benchmark(\"django-components\", \"lg\", \"startup\", context_mode)", + "min_run_count": 2, + "name": "isolated vs django modes.timeraw_startup_lg", + "number": 1, + "param_names": [ + "context_mode" + ], + "params": [ + [ + "'isolated'", + "'django'" + ] + ], + "pretty_name": "startup - large", + "repeat": 0, + "rounds": 5, + "sample_time": 0.01, + "type": "time", + "unit": "seconds", + "version": "eabe311ebee4a15c5816617be12f00ec30376f7506bd668219e1c50bc897c134", + "warmup_time": -1 + }, + "version": 2 +} \ No newline at end of file diff --git a/.asv/results/ci-linux/06c89cf9-virtualenv-py3.13-django5.1-djc-core-html-parser.json b/.asv/results/ci-linux/06c89cf9-virtualenv-py3.13-django5.1-djc-core-html-parser.json new file mode 100644 index 00000000..edbf55d8 --- /dev/null +++ b/.asv/results/ci-linux/06c89cf9-virtualenv-py3.13-django5.1-djc-core-html-parser.json @@ -0,0 +1 @@ +{"commit_hash": "06c89cf9e89a432197a4cabb5a1d6864dd6089ac", "env_name": "virtualenv-py3.13-django5.1-djc-core-html-parser", "date": 1749546769000, "params": {"machine": "ci-linux", "python": "3.13", "django": "5.1", "djc-core-html-parser": ""}, "python": "3.13", "requirements": {"django": "5.1", "djc-core-html-parser": ""}, "env_vars": {}, "result_columns": ["result", "params", "version", "started_at", "duration", "stats_ci_99_a", "stats_ci_99_b", "stats_q_25", "stats_q_75", "stats_number", "stats_repeat", "samples", "profile"], "results": {"Components vs Django.timeraw_render_lg_first": [[0.08026317200000221, 0.26819844099998136], [["'django'", "'django-components'"]], "be3bf6236960046a028b6ea007aad28b2337fc2b906b8ce317a09a5d4f1a6193", 1749547226154, 31.969, [0.079592, 0.26712], [0.081627, 0.27054], [0.079806, 0.26758], [0.080908, 0.27014], [1, 1], [25, 25]], "Components vs Django.timeraw_render_lg_subsequent": [[0.04349111400000538, 0.15453611999998884], [["'django'", "'django-components'"]], "b98221c11a0ee6e9de0778d416d31b9dd514a674d9017a2bb9b2fc1cd0f01920", 1749547232510, 37.93, [0.043083, 0.15395], [0.044645, 0.15628], [0.04313, 0.15396], [0.044614, 0.15596], [1, 1], [25, 25]], "Components vs Django.timeraw_render_sm_first": [[0.0036145729999930154, 0.004811176000004025], [["'django'", "'django-components'"]], "f1fc17e4a31c71f4d9265f1122da52e7cf57addb4dfa02606e303b33d6431b9b", 1749547240091, 17.086, [0.0035701, 0.0047897], [0.0036399, 0.0048885], [0.0035736, 0.004795], [0.0036371, 0.0048827], [1, 1], [25, 25]], "Components vs Django.timeraw_render_sm_subsequent": [[0.00012332000000014887, 0.0005915369999911491], [["'django'", "'django-components'"]], "6fce1cd85a9344fee383b40a22f27862120b9488a628420625592dc14e0307d3", 1749547243508, 17.13, [0.00012078, 0.0005805], [0.00013759, 0.00060184], [0.00012127, 0.00058107], [0.00012742, 0.00059879], [1, 1], [25, 25]], "Components vs Django.timeraw_startup_lg": [[0.2252378419999843, 0.2247263650000093], [["'django'", "'django-components'"]], "53151821c128ad0ecfb0707fff3146e1abd8d0bcfa301aa056b5d3fae3d793e2", 1749547246927, 19.999, [0.22441, 0.22363], [0.22729, 0.22686], [0.22452, 0.22369], [0.22639, 0.22556], [1, 1], [25, 25]], "Other.timeraw_import_time": [[0.20468290799999522], [], "a0a1c1c0db22509410b946d0d4384b52ea4a09b47b6048d7d1cfb89b0c7fe5c3", 1749547250936, 8.1567, [0.20338], [0.20624], [0.20374], [0.20582], [1], [25]], "isolated vs django modes.timeraw_render_lg_first": [[0.26819597400000816, 0.2740507329999957], [["'isolated'", "'django'"]], "f94af83427c6346f88f8785a3cd2fc42415ac5a9fbbdb7de71d27e22e6a81699", 1749547255664, 37.318, [0.26688, 0.27234], [0.26941, 0.28032], [0.26702, 0.2725], [0.26926, 0.279], [1, 1], [25, 25]], "isolated vs django modes.timeraw_render_lg_subsequent": [[0.15396625699997912, 0.16023626799997714], [["'isolated'", "'django'"]], "9f7c2fde6b33f0451a1794ed903c48d96cd7822f67da502cec36fe8e977c2414", 1749547263180, 47.125, [0.15282, 0.15877], [0.15647, 0.16443], [0.15297, 0.15902], [0.15588, 0.16441], [1, 1], [25, 25]], "isolated vs django modes.timeraw_render_sm_first": [[0.004825538000005736, 0.0047952310000027865], [["'isolated'", "'django'"]], "d15ca68909d7f1f43ff16863befb6f42681f17461417fc0069eefd6db3569296", 1749547272622, 17.085, [0.0048028, 0.0047649], [0.0048467, 0.0048197], [0.0048091, 0.0047651], [0.0048402, 0.0048192], [1, 1], [25, 25]], "isolated vs django modes.timeraw_render_sm_subsequent": [[0.0005953940000154034, 0.0005933700000468889], [["'isolated'", "'django'"]], "7444bc9516dd087e3f420349345eae991ad6941bbd22fce45265b18034b7cf77", 1749547276032, 17.147, [0.0005815, 0.00058741], [0.00060108, 0.00059832], [0.00058285, 0.0005878], [0.0006001, 0.00059759], [1, 1], [25, 25]], "isolated vs django modes.timeraw_startup_lg": [[0.22564571399999522, 0.22598634599995648], [["'isolated'", "'django'"]], "eabe311ebee4a15c5816617be12f00ec30376f7506bd668219e1c50bc897c134", 1749547279452, 19.816, [0.22324, 0.22336], [0.22965, 0.22994], [0.22384, 0.22382], [0.22963, 0.22889], [1, 1], [25, 25]], "Components vs Django.peakmem_render_lg_first": [[52932608, 55693312], [["'django'", "'django-components'"]], "301c396f017f45a5b3f71e85df58d15f54153fcfd951af7ef424641d4b31b528", 1749547223611, 0.79781], "Components vs Django.peakmem_render_lg_subsequent": [[53063680, 56180736], [["'django'", "'django-components'"]], "9a44e9999ef3ef42ea7e01323727490244febb43d66a87a4d8f88c6b8a133b8b", 1749547224409, 0.99222], "Components vs Django.peakmem_render_sm_first": [[44716032, 44834816], [["'django'", "'django-components'"]], "e93b7a5193681c883edf85bdb30b1bc0821263bf51033fdcee215b155085e036", 1749547225402, 0.37525], "Components vs Django.peakmem_render_sm_subsequent": [[44716032, 44838912], [["'django'", "'django-components'"]], "b46e0820b18950aa7cc5e61306ff3425b76b4da9dca42d64fae5b1d25c6c9026", 1749547225778, 0.37627], "isolated vs django modes.peakmem_render_lg_first": [[55263232, 54693888], [["'isolated'", "'django'"]], "c4bf0016d48d210f08b8db733b57c7dcba1cebbf548c458b93b86ace387067e9", 1749547252571, 1.0035], "isolated vs django modes.peakmem_render_lg_subsequent": [[55783424, 56160256], [["'isolated'", "'django'"]], "65bb1b8586487197a79bb6073e4c71642877b845b6eb42d1bd32398299daffbf", 1749547253575, 1.3187], "isolated vs django modes.peakmem_render_sm_first": [[44838912, 44838912], [["'isolated'", "'django'"]], "c51b91fc583295776062822225e720b5ed71aef9c9288217c401c54283c62840", 1749547254894, 0.38522], "isolated vs django modes.peakmem_render_sm_subsequent": [[44965888, 44834816], [["'isolated'", "'django'"]], "54d747fb8f40179b7ff3d2fc49eb195909ad1c880b5ef7b82f82742b27b67260", 1749547255279, 0.38471]}, "durations": {}, "version": 2} \ No newline at end of file diff --git a/.asv/results/ci-linux/07f747d7-virtualenv-py3.13-django5.1-djc-core-html-parser.json b/.asv/results/ci-linux/07f747d7-virtualenv-py3.13-django5.1-djc-core-html-parser.json new file mode 100644 index 00000000..a7ada305 --- /dev/null +++ b/.asv/results/ci-linux/07f747d7-virtualenv-py3.13-django5.1-djc-core-html-parser.json @@ -0,0 +1 @@ +{"commit_hash": "07f747d70500bbe3e135725b0e1b102815ab9416", "env_name": "virtualenv-py3.13-django5.1-djc-core-html-parser", "date": 1744216267000, "params": {"machine": "ci-linux", "python": "3.13", "django": "5.1", "djc-core-html-parser": ""}, "python": "3.13", "requirements": {"django": "5.1", "djc-core-html-parser": ""}, "env_vars": {}, "result_columns": ["result", "params", "version", "started_at", "duration", "stats_ci_99_a", "stats_ci_99_b", "stats_q_25", "stats_q_75", "stats_number", "stats_repeat", "samples", "profile"], "results": {"Components vs Django.timeraw_render_lg_first": [[0.0749189080000292, 0.26436952000000247], [["'django'", "'django-components'"]], "be3bf6236960046a028b6ea007aad28b2337fc2b906b8ce317a09a5d4f1a6193", 1744216619486, 33.178, [0.07366, 0.26251], [0.075619, 0.26728], [0.073675, 0.26261], [0.075618, 0.26698], [1, 1], [25, 25]], "Components vs Django.timeraw_render_lg_subsequent": [[0.03752758399997447, 0.15356457899997622], [["'django'", "'django-components'"]], "b98221c11a0ee6e9de0778d416d31b9dd514a674d9017a2bb9b2fc1cd0f01920", 1744216625947, 38.386, [0.037126, 0.15125], [0.038391, 0.15573], [0.037176, 0.15202], [0.038319, 0.15564], [1, 1], [25, 25]], "Components vs Django.timeraw_render_sm_first": [[0.0036766670000361046, 0.004929383999979109], [["'django'", "'django-components'"]], "f1fc17e4a31c71f4d9265f1122da52e7cf57addb4dfa02606e303b33d6431b9b", 1744216633574, 17.798, [0.0036349, 0.0048414], [0.0037472, 0.0050383], [0.0036356, 0.0048456], [0.0037349, 0.0049943], [1, 1], [25, 25]], "Components vs Django.timeraw_render_sm_subsequent": [[0.0001176700000087294, 0.0005864990000077341], [["'django'", "'django-components'"]], "6fce1cd85a9344fee383b40a22f27862120b9488a628420625592dc14e0307d3", 1744216637046, 17.63, [0.00011614, 0.00057608], [0.00012402, 0.00060528], [0.00011638, 0.00058095], [0.00012214, 0.00060196], [1, 1], [25, 25]], "Components vs Django.timeraw_startup_lg": [[0.22985272800002576, 0.22544496099999378], [["'django'", "'django-components'"]], "53151821c128ad0ecfb0707fff3146e1abd8d0bcfa301aa056b5d3fae3d793e2", 1744216640541, 20.874, [0.2257, 0.22291], [0.23126, 0.22794], [0.22584, 0.2231], [0.23102, 0.22667], [1, 1], [25, 25]], "Other.timeraw_import_time": [[0.20376683500001036], [], "a0a1c1c0db22509410b946d0d4384b52ea4a09b47b6048d7d1cfb89b0c7fe5c3", 1744216644673, 8.4126, [0.20226], [0.20665], [0.20247], [0.20632], [1], [25]], "isolated vs django modes.timeraw_render_lg_first": [[0.265977821000007, 0.26914772099999595], [["'isolated'", "'django'"]], "f94af83427c6346f88f8785a3cd2fc42415ac5a9fbbdb7de71d27e22e6a81699", 1744216649436, 38.086, [0.26238, 0.26661], [0.26855, 0.2726], [0.26253, 0.26679], [0.26818, 0.27222], [1, 1], [25, 25]], "isolated vs django modes.timeraw_render_lg_subsequent": [[0.15150350199996865, 0.1558047899999906], [["'isolated'", "'django'"]], "9f7c2fde6b33f0451a1794ed903c48d96cd7822f67da502cec36fe8e977c2414", 1744216657009, 47.487, [0.15077, 0.15425], [0.1542, 0.15864], [0.15093, 0.15461], [0.15373, 0.15862], [1, 1], [25, 25]], "isolated vs django modes.timeraw_render_sm_first": [[0.004901490999998259, 0.004895917999988342], [["'isolated'", "'django'"]], "d15ca68909d7f1f43ff16863befb6f42681f17461417fc0069eefd6db3569296", 1744216666440, 17.693, [0.0048488, 0.0048302], [0.0049534, 0.0049506], [0.004858, 0.0048375], [0.0049529, 0.0049476], [1, 1], [25, 25]], "isolated vs django modes.timeraw_render_sm_subsequent": [[0.0005929909999622396, 0.000583071999983531], [["'isolated'", "'django'"]], "7444bc9516dd087e3f420349345eae991ad6941bbd22fce45265b18034b7cf77", 1744216669983, 17.728, [0.00057117, 0.00057317], [0.00060263, 0.0006142], [0.00057741, 0.00057397], [0.00060104, 0.00060429], [1, 1], [25, 25]], "isolated vs django modes.timeraw_startup_lg": [[0.22394285699999728, 0.22330144500000415], [["'isolated'", "'django'"]], "eabe311ebee4a15c5816617be12f00ec30376f7506bd668219e1c50bc897c134", 1744216673490, 20.287, [0.22183, 0.22116], [0.22692, 0.2269], [0.22188, 0.22124], [0.22587, 0.22687], [1, 1], [25, 25]], "Components vs Django.peakmem_render_lg_first": [[53047296, 55373824], [["'django'", "'django-components'"]], "301c396f017f45a5b3f71e85df58d15f54153fcfd951af7ef424641d4b31b528", 1744216616926, 0.79581], "Components vs Django.peakmem_render_lg_subsequent": [[53948416, 55824384], [["'django'", "'django-components'"]], "9a44e9999ef3ef42ea7e01323727490244febb43d66a87a4d8f88c6b8a133b8b", 1744216617722, 1.0125], "Components vs Django.peakmem_render_sm_first": [[44326912, 44322816], [["'django'", "'django-components'"]], "e93b7a5193681c883edf85bdb30b1bc0821263bf51033fdcee215b155085e036", 1744216618735, 0.37296], "Components vs Django.peakmem_render_sm_subsequent": [[44101632, 44310528], [["'django'", "'django-components'"]], "b46e0820b18950aa7cc5e61306ff3425b76b4da9dca42d64fae5b1d25c6c9026", 1744216619108, 0.37753], "isolated vs django modes.peakmem_render_lg_first": [[55246848, 54898688], [["'isolated'", "'django'"]], "c4bf0016d48d210f08b8db733b57c7dcba1cebbf548c458b93b86ace387067e9", 1744216646319, 1.0346], "isolated vs django modes.peakmem_render_lg_subsequent": [[55812096, 55611392], [["'isolated'", "'django'"]], "65bb1b8586487197a79bb6073e4c71642877b845b6eb42d1bd32398299daffbf", 1744216647354, 1.3258], "isolated vs django modes.peakmem_render_sm_first": [[44322816, 44314624], [["'isolated'", "'django'"]], "c51b91fc583295776062822225e720b5ed71aef9c9288217c401c54283c62840", 1744216648680, 0.37601], "isolated vs django modes.peakmem_render_sm_subsequent": [[44314624, 44318720], [["'isolated'", "'django'"]], "54d747fb8f40179b7ff3d2fc49eb195909ad1c880b5ef7b82f82742b27b67260", 1744216649056, 0.37944]}, "durations": {}, "version": 2} \ No newline at end of file diff --git a/.asv/results/ci-linux/1319a956-virtualenv-py3.13-django5.1-djc-core-html-parser.json b/.asv/results/ci-linux/1319a956-virtualenv-py3.13-django5.1-djc-core-html-parser.json new file mode 100644 index 00000000..a92be098 --- /dev/null +++ b/.asv/results/ci-linux/1319a956-virtualenv-py3.13-django5.1-djc-core-html-parser.json @@ -0,0 +1 @@ +{"commit_hash": "1319a95627493fc0745b5af0600af2dc8c5117f9", "env_name": "virtualenv-py3.13-django5.1-djc-core-html-parser", "date": 1744204166000, "params": {"machine": "ci-linux", "python": "3.13", "django": "5.1", "djc-core-html-parser": ""}, "python": "3.13", "requirements": {"django": "5.1", "djc-core-html-parser": ""}, "env_vars": {}, "result_columns": ["result", "params", "version", "started_at", "duration", "stats_ci_99_a", "stats_ci_99_b", "stats_q_25", "stats_q_75", "stats_number", "stats_repeat", "samples", "profile"], "results": {"Components vs Django.timeraw_render_lg_first": [[0.07308550800001967, 0.26274096600002395], [["'django'", "'django-components'"]], "be3bf6236960046a028b6ea007aad28b2337fc2b906b8ce317a09a5d4f1a6193", 1744204892087, 32.138, [0.072587, 0.26115], [0.074192, 0.26642], [0.072641, 0.26122], [0.073973, 0.26591], [1, 1], [25, 25]], "Components vs Django.timeraw_render_lg_subsequent": [[0.03723830100000214, 0.15196534300002895], [["'django'", "'django-components'"]], "b98221c11a0ee6e9de0778d416d31b9dd514a674d9017a2bb9b2fc1cd0f01920", 1744204898443, 37.515, [0.036587, 0.1496], [0.037873, 0.15446], [0.036668, 0.14991], [0.037851, 0.15324], [1, 1], [25, 25]], "Components vs Django.timeraw_render_sm_first": [[0.003566315000000486, 0.004791812000007667], [["'django'", "'django-components'"]], "f1fc17e4a31c71f4d9265f1122da52e7cf57addb4dfa02606e303b33d6431b9b", 1744204905997, 17.018, [0.0035472, 0.0047455], [0.0036147, 0.0048525], [0.0035495, 0.0047533], [0.0036128, 0.0048458], [1, 1], [25, 25]], "Components vs Django.timeraw_render_sm_subsequent": [[0.00011609900002440554, 0.0005779780000239043], [["'django'", "'django-components'"]], "6fce1cd85a9344fee383b40a22f27862120b9488a628420625592dc14e0307d3", 1744204909393, 17.165, [0.00011522, 0.00056305], [0.00011862, 0.00060154], [0.00011551, 0.00056367], [0.00011789, 0.0005929], [1, 1], [25, 25]], "Components vs Django.timeraw_startup_lg": [[0.22339861599999722, 0.22020213500002228], [["'django'", "'django-components'"]], "53151821c128ad0ecfb0707fff3146e1abd8d0bcfa301aa056b5d3fae3d793e2", 1744204912773, 20.145, [0.22184, 0.21813], [0.22706, 0.22287], [0.22223, 0.21861], [0.22593, 0.22095], [1, 1], [25, 25]], "Other.timeraw_import_time": [[0.20073733000003813], [], "a0a1c1c0db22509410b946d0d4384b52ea4a09b47b6048d7d1cfb89b0c7fe5c3", 1744204916751, 8.1154, [0.1995], [0.2041], [0.19959], [0.20363], [1], [25]], "isolated vs django modes.timeraw_render_lg_first": [[0.262679922000018, 0.2686107789999994], [["'isolated'", "'django'"]], "f94af83427c6346f88f8785a3cd2fc42415ac5a9fbbdb7de71d27e22e6a81699", 1744204921359, 37.333, [0.25985, 0.2655], [0.2667, 0.27137], [0.26015, 0.26588], [0.26589, 0.27026], [1, 1], [25, 25]], "isolated vs django modes.timeraw_render_lg_subsequent": [[0.15071133700001837, 0.15540660900001058], [["'isolated'", "'django'"]], "9f7c2fde6b33f0451a1794ed903c48d96cd7822f67da502cec36fe8e977c2414", 1744204928788, 47.002, [0.14963, 0.15366], [0.15432, 0.15808], [0.14986, 0.15409], [0.15418, 0.15789], [1, 1], [25, 25]], "isolated vs django modes.timeraw_render_sm_first": [[0.00478528000002143, 0.0047485900000197034], [["'isolated'", "'django'"]], "d15ca68909d7f1f43ff16863befb6f42681f17461417fc0069eefd6db3569296", 1744204938238, 17.062, [0.004736, 0.0047199], [0.0048543, 0.004812], [0.0047396, 0.0047303], [0.0048316, 0.0047851], [1, 1], [25, 25]], "isolated vs django modes.timeraw_render_sm_subsequent": [[0.0005853119999983392, 0.000582014999963576], [["'isolated'", "'django'"]], "7444bc9516dd087e3f420349345eae991ad6941bbd22fce45265b18034b7cf77", 1744204941632, 17.123, [0.00056611, 0.00057364], [0.00059521, 0.00058681], [0.00056804, 0.00057454], [0.00059519, 0.00058642], [1, 1], [25, 25]], "isolated vs django modes.timeraw_startup_lg": [[0.22056839900000114, 0.21916191200000412], [["'isolated'", "'django'"]], "eabe311ebee4a15c5816617be12f00ec30376f7506bd668219e1c50bc897c134", 1744204945049, 19.625, [0.21938, 0.21844], [0.22222, 0.22102], [0.21966, 0.21859], [0.22174, 0.221], [1, 1], [25, 25]], "Components vs Django.peakmem_render_lg_first": [[53084160, 55382016], [["'django'", "'django-components'"]], "301c396f017f45a5b3f71e85df58d15f54153fcfd951af7ef424641d4b31b528", 1744204889613, 0.78176], "Components vs Django.peakmem_render_lg_subsequent": [[53739520, 55812096], [["'django'", "'django-components'"]], "9a44e9999ef3ef42ea7e01323727490244febb43d66a87a4d8f88c6b8a133b8b", 1744204890395, 0.96208], "Components vs Django.peakmem_render_sm_first": [[44322816, 44314624], [["'django'", "'django-components'"]], "e93b7a5193681c883edf85bdb30b1bc0821263bf51033fdcee215b155085e036", 1744204891357, 0.36135], "Components vs Django.peakmem_render_sm_subsequent": [[44322816, 44310528], [["'django'", "'django-components'"]], "b46e0820b18950aa7cc5e61306ff3425b76b4da9dca42d64fae5b1d25c6c9026", 1744204891719, 0.36764], "isolated vs django modes.peakmem_render_lg_first": [[55373824, 54894592], [["'isolated'", "'django'"]], "c4bf0016d48d210f08b8db733b57c7dcba1cebbf548c458b93b86ace387067e9", 1744204918353, 0.96762], "isolated vs django modes.peakmem_render_lg_subsequent": [[55545856, 55631872], [["'isolated'", "'django'"]], "65bb1b8586487197a79bb6073e4c71642877b845b6eb42d1bd32398299daffbf", 1744204919321, 1.2917], "isolated vs django modes.peakmem_render_sm_first": [[44318720, 44314624], [["'isolated'", "'django'"]], "c51b91fc583295776062822225e720b5ed71aef9c9288217c401c54283c62840", 1744204920613, 0.3751], "isolated vs django modes.peakmem_render_sm_subsequent": [[44310528, 44314624], [["'isolated'", "'django'"]], "54d747fb8f40179b7ff3d2fc49eb195909ad1c880b5ef7b82f82742b27b67260", 1744204920988, 0.37048]}, "durations": {}, "version": 2} \ No newline at end of file diff --git a/.asv/results/ci-linux/2037ed20-virtualenv-py3.13-django5.1-djc-core-html-parser.json b/.asv/results/ci-linux/2037ed20-virtualenv-py3.13-django5.1-djc-core-html-parser.json new file mode 100644 index 00000000..b6c6accf --- /dev/null +++ b/.asv/results/ci-linux/2037ed20-virtualenv-py3.13-django5.1-djc-core-html-parser.json @@ -0,0 +1 @@ +{"commit_hash": "2037ed20b7252cc52008e69bdd3d8e095ad6ea08", "env_name": "virtualenv-py3.13-django5.1-djc-core-html-parser", "date": 1742645505000, "params": {"machine": "ci-linux", "python": "3.13", "django": "5.1", "djc-core-html-parser": ""}, "python": "3.13", "requirements": {"django": "5.1", "djc-core-html-parser": ""}, "env_vars": {}, "result_columns": ["result", "params", "version", "started_at", "duration", "stats_ci_99_a", "stats_ci_99_b", "stats_q_25", "stats_q_75", "stats_number", "stats_repeat", "samples", "profile"], "results": {"Components vs Django.timeraw_render_lg_first": [[0.07114163800000028, 0.26389872900000455], [["'django'", "'django-components'"]], "be3bf6236960046a028b6ea007aad28b2337fc2b906b8ce317a09a5d4f1a6193", 1742645992571, 32.683, [0.069774, 0.25821], [0.073121, 0.27402], [0.069851, 0.25974], [0.072998, 0.26645], [1, 1], [25, 25]], "Components vs Django.timeraw_render_lg_subsequent": [[0.033918617999972867, 0.14395761299999776], [["'django'", "'django-components'"]], "b98221c11a0ee6e9de0778d416d31b9dd514a674d9017a2bb9b2fc1cd0f01920", 1742645999069, 37.874, [0.033132, 0.14143], [0.03718, 0.14845], [0.033146, 0.14223], [0.036758, 0.14717], [1, 1], [25, 25]], "Components vs Django.timeraw_render_sm_first": [[0.0036137869999777195, 0.004807943000002979], [["'django'", "'django-components'"]], "f1fc17e4a31c71f4d9265f1122da52e7cf57addb4dfa02606e303b33d6431b9b", 1742646006580, 17.577, [0.0035726, 0.0047711], [0.0037623, 0.005014], [0.0035734, 0.0047729], [0.0037603, 0.0049865], [1, 1], [25, 25]], "Components vs Django.timeraw_render_sm_subsequent": [[0.00010086800000408402, 0.0005549249999887707], [["'django'", "'django-components'"]], "6fce1cd85a9344fee383b40a22f27862120b9488a628420625592dc14e0307d3", 1742646010016, 17.508, [9.8133e-05, 0.00053722], [0.00010873, 0.00056563], [9.8243e-05, 0.00053799], [0.00010711, 0.00056262], [1, 1], [25, 25]], "Components vs Django.timeraw_startup_lg": [[0.22476057199997967, 0.22048105400000395], [["'django'", "'django-components'"]], "53151821c128ad0ecfb0707fff3146e1abd8d0bcfa301aa056b5d3fae3d793e2", 1742646013452, 20.612, [0.22251, 0.21707], [0.22804, 0.22459], [0.22271, 0.21711], [0.22783, 0.22416], [1, 1], [25, 25]], "Other.timeraw_import_time": [[0.20217585500000723], [], "a0a1c1c0db22509410b946d0d4384b52ea4a09b47b6048d7d1cfb89b0c7fe5c3", 1742646017529, 8.2678, [0.20055], [0.20485], [0.20064], [0.20445], [1], [25]], "isolated vs django modes.timeraw_render_lg_first": [[0.2600247560000071, 0.26185358800000813], [["'isolated'", "'django'"]], "f94af83427c6346f88f8785a3cd2fc42415ac5a9fbbdb7de71d27e22e6a81699", 1742646022211, 37.548, [0.25553, 0.25903], [0.26378, 0.26742], [0.25572, 0.25907], [0.26268, 0.26457], [1, 1], [25, 25]], "isolated vs django modes.timeraw_render_lg_subsequent": [[0.14515931700000806, 0.14909453600000688], [["'isolated'", "'django'"]], "9f7c2fde6b33f0451a1794ed903c48d96cd7822f67da502cec36fe8e977c2414", 1742646029641, 46.958, [0.14261, 0.147], [0.14958, 0.15597], [0.1427, 0.14746], [0.14895, 0.15119], [1, 1], [25, 25]], "isolated vs django modes.timeraw_render_sm_first": [[0.004856270999994194, 0.00490694800001279], [["'isolated'", "'django'"]], "d15ca68909d7f1f43ff16863befb6f42681f17461417fc0069eefd6db3569296", 1742646038981, 17.673, [0.0047454, 0.0047974], [0.0050872, 0.0050523], [0.0047616, 0.0048062], [0.005, 0.0050457], [1, 1], [25, 25]], "isolated vs django modes.timeraw_render_sm_subsequent": [[0.000547750000009728, 0.0005677989999810507], [["'isolated'", "'django'"]], "7444bc9516dd087e3f420349345eae991ad6941bbd22fce45265b18034b7cf77", 1742646042461, 17.734, [0.00053762, 0.00054036], [0.00056353, 0.0005841], [0.00053867, 0.00054808], [0.00055477, 0.00058339], [1, 1], [25, 25]], "isolated vs django modes.timeraw_startup_lg": [[0.2221746719999942, 0.2222580240000127], [["'isolated'", "'django'"]], "eabe311ebee4a15c5816617be12f00ec30376f7506bd668219e1c50bc897c134", 1742646045952, 20.444, [0.21901, 0.2187], [0.22925, 0.22649], [0.22017, 0.21932], [0.22825, 0.22535], [1, 1], [25, 25]], "Components vs Django.peakmem_render_lg_first": [[52350976, 54599680], [["'django'", "'django-components'"]], "301c396f017f45a5b3f71e85df58d15f54153fcfd951af7ef424641d4b31b528", 1742645990033, 0.81782], "Components vs Django.peakmem_render_lg_subsequent": [[52289536, 55099392], [["'django'", "'django-components'"]], "9a44e9999ef3ef42ea7e01323727490244febb43d66a87a4d8f88c6b8a133b8b", 1742645990851, 0.96118], "Components vs Django.peakmem_render_sm_first": [[44056576, 44048384], [["'django'", "'django-components'"]], "e93b7a5193681c883edf85bdb30b1bc0821263bf51033fdcee215b155085e036", 1742645991813, 0.37787], "Components vs Django.peakmem_render_sm_subsequent": [[44060672, 43917312], [["'django'", "'django-components'"]], "b46e0820b18950aa7cc5e61306ff3425b76b4da9dca42d64fae5b1d25c6c9026", 1742645992191, 0.38003], "isolated vs django modes.peakmem_render_lg_first": [[54616064, 54140928], [["'isolated'", "'django'"]], "c4bf0016d48d210f08b8db733b57c7dcba1cebbf548c458b93b86ace387067e9", 1742646019154, 0.98924], "isolated vs django modes.peakmem_render_lg_subsequent": [[54849536, 54841344], [["'isolated'", "'django'"]], "65bb1b8586487197a79bb6073e4c71642877b845b6eb42d1bd32398299daffbf", 1742646020143, 1.3166], "isolated vs django modes.peakmem_render_sm_first": [[44048384, 44048384], [["'isolated'", "'django'"]], "c51b91fc583295776062822225e720b5ed71aef9c9288217c401c54283c62840", 1742646021460, 0.37686], "isolated vs django modes.peakmem_render_sm_subsequent": [[44052480, 44052480], [["'isolated'", "'django'"]], "54d747fb8f40179b7ff3d2fc49eb195909ad1c880b5ef7b82f82742b27b67260", 1742646021838, 0.37297]}, "durations": {}, "version": 2} \ No newline at end of file diff --git a/.asv/results/ci-linux/2472c2ad-virtualenv-py3.13-django5.1-djc-core-html-parser.json b/.asv/results/ci-linux/2472c2ad-virtualenv-py3.13-django5.1-djc-core-html-parser.json new file mode 100644 index 00000000..48ce9ff5 --- /dev/null +++ b/.asv/results/ci-linux/2472c2ad-virtualenv-py3.13-django5.1-djc-core-html-parser.json @@ -0,0 +1 @@ +{"commit_hash": "2472c2ad338a23fba015d4d9816cb62d1325455f", "env_name": "virtualenv-py3.13-django5.1-djc-core-html-parser", "date": 1742720064000, "params": {"machine": "ci-linux", "python": "3.13", "django": "5.1", "djc-core-html-parser": ""}, "python": "3.13", "requirements": {"django": "5.1", "djc-core-html-parser": ""}, "env_vars": {}, "result_columns": ["result", "params", "version", "started_at", "duration", "stats_ci_99_a", "stats_ci_99_b", "stats_q_25", "stats_q_75", "stats_number", "stats_repeat", "samples", "profile"], "results": {"Components vs Django.timeraw_render_lg_first": [[0.06910802600003763, 0.25746033199999374], [["'django'", "'django-components'"]], "be3bf6236960046a028b6ea007aad28b2337fc2b906b8ce317a09a5d4f1a6193", 1742720448344, 30.848, [0.068873, 0.25443], [0.070492, 0.26059], [0.068879, 0.25485], [0.069904, 0.25905], [1, 1], [25, 25]], "Components vs Django.timeraw_render_lg_subsequent": [[0.03317536700001256, 0.14245594600001255], [["'django'", "'django-components'"]], "b98221c11a0ee6e9de0778d416d31b9dd514a674d9017a2bb9b2fc1cd0f01920", 1742720454523, 36.264, [0.032774, 0.14105], [0.034001, 0.14496], [0.032835, 0.14147], [0.033931, 0.14484], [1, 1], [25, 25]], "Components vs Django.timeraw_render_sm_first": [[0.0035223549999727766, 0.004706463999980315], [["'django'", "'django-components'"]], "f1fc17e4a31c71f4d9265f1122da52e7cf57addb4dfa02606e303b33d6431b9b", 1742720461726, 16.586, [0.0034935, 0.0046795], [0.0035519, 0.0047641], [0.0034979, 0.0046875], [0.0035513, 0.0047525], [1, 1], [25, 25]], "Components vs Django.timeraw_render_sm_subsequent": [[9.818199998790078e-05, 0.0005511469999817109], [["'django'", "'django-components'"]], "6fce1cd85a9344fee383b40a22f27862120b9488a628420625592dc14e0307d3", 1742720465042, 16.608, [9.7351e-05, 0.00054462], [9.9656e-05, 0.00055608], [9.7582e-05, 0.00054577], [9.9445e-05, 0.00055587], [1, 1], [25, 25]], "Components vs Django.timeraw_startup_lg": [[0.21809406599999193, 0.2131839880000257], [["'django'", "'django-components'"]], "53151821c128ad0ecfb0707fff3146e1abd8d0bcfa301aa056b5d3fae3d793e2", 1742720468367, 19.52, [0.21698, 0.21208], [0.21947, 0.21482], [0.21721, 0.21251], [0.21904, 0.21475], [1, 1], [25, 25]], "Other.timeraw_import_time": [[0.19726691500000015], [], "a0a1c1c0db22509410b946d0d4384b52ea4a09b47b6048d7d1cfb89b0c7fe5c3", 1742720472264, 7.9149, [0.19686], [0.1978], [0.19687], [0.19779], [1], [25]], "isolated vs django modes.timeraw_render_lg_first": [[0.2567828300000201, 0.2602957870000182], [["'isolated'", "'django'"]], "f94af83427c6346f88f8785a3cd2fc42415ac5a9fbbdb7de71d27e22e6a81699", 1742720476744, 36.159, [0.25481, 0.25713], [0.26079, 0.26229], [0.25484, 0.25769], [0.26043, 0.26225], [1, 1], [25, 25]], "isolated vs django modes.timeraw_render_lg_subsequent": [[0.1423055980000072, 0.14642362500001127], [["'isolated'", "'django'"]], "9f7c2fde6b33f0451a1794ed903c48d96cd7822f67da502cec36fe8e977c2414", 1742720484000, 45.045, [0.14132, 0.14533], [0.14548, 0.14785], [0.14152, 0.14539], [0.14515, 0.14779], [1, 1], [25, 25]], "isolated vs django modes.timeraw_render_sm_first": [[0.00473016699999107, 0.004734037999980956], [["'isolated'", "'django'"]], "d15ca68909d7f1f43ff16863befb6f42681f17461417fc0069eefd6db3569296", 1742720493008, 16.726, [0.0046832, 0.0046931], [0.004798, 0.0047607], [0.0046901, 0.0046966], [0.0047946, 0.0047554], [1, 1], [25, 25]], "isolated vs django modes.timeraw_render_sm_subsequent": [[0.0005471899999918151, 0.0005447550000212686], [["'isolated'", "'django'"]], "7444bc9516dd087e3f420349345eae991ad6941bbd22fce45265b18034b7cf77", 1742720496354, 16.725, [0.00053011, 0.00053386], [0.00056137, 0.00056687], [0.00053047, 0.00053669], [0.00055674, 0.00055495], [1, 1], [25, 25]], "isolated vs django modes.timeraw_startup_lg": [[0.2142312400000037, 0.21397752699999728], [["'isolated'", "'django'"]], "eabe311ebee4a15c5816617be12f00ec30376f7506bd668219e1c50bc897c134", 1742720499699, 19.132, [0.21291, 0.213], [0.21765, 0.21644], [0.21297, 0.21307], [0.21719, 0.21607], [1, 1], [25, 25]], "Components vs Django.peakmem_render_lg_first": [[52109312, 54779904], [["'django'", "'django-components'"]], "301c396f017f45a5b3f71e85df58d15f54153fcfd951af7ef424641d4b31b528", 1742720445954, 0.7513], "Components vs Django.peakmem_render_lg_subsequent": [[52142080, 55255040], [["'django'", "'django-components'"]], "9a44e9999ef3ef42ea7e01323727490244febb43d66a87a4d8f88c6b8a133b8b", 1742720446706, 0.92703], "Components vs Django.peakmem_render_sm_first": [[44191744, 44310528], [["'django'", "'django-components'"]], "e93b7a5193681c883edf85bdb30b1bc0821263bf51033fdcee215b155085e036", 1742720447633, 0.35395], "Components vs Django.peakmem_render_sm_subsequent": [[44105728, 44310528], [["'django'", "'django-components'"]], "b46e0820b18950aa7cc5e61306ff3425b76b4da9dca42d64fae5b1d25c6c9026", 1742720447987, 0.3569], "isolated vs django modes.peakmem_render_lg_first": [[54767616, 54296576], [["'isolated'", "'django'"]], "c4bf0016d48d210f08b8db733b57c7dcba1cebbf548c458b93b86ace387067e9", 1742720473841, 0.94212], "isolated vs django modes.peakmem_render_lg_subsequent": [[55271424, 55304192], [["'isolated'", "'django'"]], "65bb1b8586487197a79bb6073e4c71642877b845b6eb42d1bd32398299daffbf", 1742720474783, 1.2374], "isolated vs django modes.peakmem_render_sm_first": [[44314624, 44310528], [["'isolated'", "'django'"]], "c51b91fc583295776062822225e720b5ed71aef9c9288217c401c54283c62840", 1742720476021, 0.36127], "isolated vs django modes.peakmem_render_sm_subsequent": [[44314624, 44310528], [["'isolated'", "'django'"]], "54d747fb8f40179b7ff3d2fc49eb195909ad1c880b5ef7b82f82742b27b67260", 1742720476383, 0.36101]}, "durations": {}, "version": 2} \ No newline at end of file diff --git a/.asv/results/ci-linux/42818ad6-virtualenv-py3.13-django5.1-djc-core-html-parser.json b/.asv/results/ci-linux/42818ad6-virtualenv-py3.13-django5.1-djc-core-html-parser.json new file mode 100644 index 00000000..53ff3ee2 --- /dev/null +++ b/.asv/results/ci-linux/42818ad6-virtualenv-py3.13-django5.1-djc-core-html-parser.json @@ -0,0 +1 @@ +{"commit_hash": "42818ad6ffb47bd650d8a379b84c3d48394f9f77", "env_name": "virtualenv-py3.13-django5.1-djc-core-html-parser", "date": 1742765538000, "params": {"machine": "ci-linux", "python": "3.13", "django": "5.1", "djc-core-html-parser": ""}, "python": "3.13", "requirements": {"django": "5.1", "djc-core-html-parser": ""}, "env_vars": {}, "result_columns": ["result", "params", "version", "started_at", "duration", "stats_ci_99_a", "stats_ci_99_b", "stats_q_25", "stats_q_75", "stats_number", "stats_repeat", "samples", "profile"], "results": {"Components vs Django.timeraw_render_lg_first": [[0.07048037500001669, 0.2598985070000026], [["'django'", "'django-components'"]], "be3bf6236960046a028b6ea007aad28b2337fc2b906b8ce317a09a5d4f1a6193", 1742765993409, 32.579, [0.069842, 0.25804], [0.071131, 0.26492], [0.069874, 0.25825], [0.071078, 0.26365], [1, 1], [25, 25]], "Components vs Django.timeraw_render_lg_subsequent": [[0.034316510999985894, 0.1444248799999741], [["'django'", "'django-components'"]], "b98221c11a0ee6e9de0778d416d31b9dd514a674d9017a2bb9b2fc1cd0f01920", 1742765999750, 37.501, [0.033683, 0.14308], [0.034786, 0.14803], [0.033696, 0.14309], [0.034745, 0.14771], [1, 1], [25, 25]], "Components vs Django.timeraw_render_sm_first": [[0.00364059099999281, 0.004926952999994683], [["'django'", "'django-components'"]], "f1fc17e4a31c71f4d9265f1122da52e7cf57addb4dfa02606e303b33d6431b9b", 1742766007246, 17.513, [0.0036032, 0.0048467], [0.0037114, 0.0049811], [0.003609, 0.0048595], [0.0036794, 0.0049768], [1, 1], [25, 25]], "Components vs Django.timeraw_render_sm_subsequent": [[0.0001005780000014056, 0.0005555879999974422], [["'django'", "'django-components'"]], "6fce1cd85a9344fee383b40a22f27862120b9488a628420625592dc14e0307d3", 1742766010749, 17.458, [9.8725e-05, 0.00054444], [0.00010239, 0.00057191], [9.8835e-05, 0.00054676], [0.00010186, 0.00056928], [1, 1], [25, 25]], "Components vs Django.timeraw_startup_lg": [[0.22356123500000535, 0.22167734499998915], [["'django'", "'django-components'"]], "53151821c128ad0ecfb0707fff3146e1abd8d0bcfa301aa056b5d3fae3d793e2", 1742766014253, 20.515, [0.2223, 0.21793], [0.22664, 0.22338], [0.22244, 0.21819], [0.22647, 0.22298], [1, 1], [25, 25]], "Other.timeraw_import_time": [[0.20350580199999513], [], "a0a1c1c0db22509410b946d0d4384b52ea4a09b47b6048d7d1cfb89b0c7fe5c3", 1742766018296, 8.3746, [0.20146], [0.20645], [0.20149], [0.20587], [1], [25]], "isolated vs django modes.timeraw_render_lg_first": [[0.259077934000004, 0.2619792840000059], [["'isolated'", "'django'"]], "f94af83427c6346f88f8785a3cd2fc42415ac5a9fbbdb7de71d27e22e6a81699", 1742766023061, 37.296, [0.25617, 0.26014], [0.26126, 0.2646], [0.25637, 0.26026], [0.26126, 0.26434], [1, 1], [25, 25]], "isolated vs django modes.timeraw_render_lg_subsequent": [[0.1436571560000175, 0.14915657599999577], [["'isolated'", "'django'"]], "9f7c2fde6b33f0451a1794ed903c48d96cd7822f67da502cec36fe8e977c2414", 1742766030546, 46.343, [0.14315, 0.14755], [0.14626, 0.15157], [0.1432, 0.14763], [0.14512, 0.15085], [1, 1], [25, 25]], "isolated vs django modes.timeraw_render_sm_first": [[0.004871503999993365, 0.0048899079999955575], [["'isolated'", "'django'"]], "d15ca68909d7f1f43ff16863befb6f42681f17461417fc0069eefd6db3569296", 1742766039800, 17.572, [0.0048252, 0.0048469], [0.0049596, 0.0049328], [0.0048256, 0.0048546], [0.0049269, 0.0049254], [1, 1], [25, 25]], "isolated vs django modes.timeraw_render_sm_subsequent": [[0.0005559489999882317, 0.0005480739999939033], [["'isolated'", "'django'"]], "7444bc9516dd087e3f420349345eae991ad6941bbd22fce45265b18034b7cf77", 1742766043333, 17.626, [0.00054656, 0.0005378], [0.00056755, 0.00057285], [0.00055044, 0.00053834], [0.00056621, 0.00056985], [1, 1], [25, 25]], "isolated vs django modes.timeraw_startup_lg": [[0.22129613300000983, 0.21942976399998315], [["'isolated'", "'django'"]], "eabe311ebee4a15c5816617be12f00ec30376f7506bd668219e1c50bc897c134", 1742766046839, 20.159, [0.2193, 0.21771], [0.22394, 0.22324], [0.21938, 0.21789], [0.22387, 0.22275], [1, 1], [25, 25]], "Components vs Django.peakmem_render_lg_first": [[52899840, 54730752], [["'django'", "'django-components'"]], "301c396f017f45a5b3f71e85df58d15f54153fcfd951af7ef424641d4b31b528", 1742765990932, 0.78775], "Components vs Django.peakmem_render_lg_subsequent": [[53796864, 55238656], [["'django'", "'django-components'"]], "9a44e9999ef3ef42ea7e01323727490244febb43d66a87a4d8f88c6b8a133b8b", 1742765991720, 0.95598], "Components vs Django.peakmem_render_sm_first": [[44183552, 44175360], [["'django'", "'django-components'"]], "e93b7a5193681c883edf85bdb30b1bc0821263bf51033fdcee215b155085e036", 1742765992676, 0.3646], "Components vs Django.peakmem_render_sm_subsequent": [[44187648, 44183552], [["'django'", "'django-components'"]], "b46e0820b18950aa7cc5e61306ff3425b76b4da9dca42d64fae5b1d25c6c9026", 1742765993041, 0.36782], "isolated vs django modes.peakmem_render_lg_first": [[54743040, 54087680], [["'isolated'", "'django'"]], "c4bf0016d48d210f08b8db733b57c7dcba1cebbf548c458b93b86ace387067e9", 1742766020000, 1.0194], "isolated vs django modes.peakmem_render_lg_subsequent": [[54984704, 54964224], [["'isolated'", "'django'"]], "65bb1b8586487197a79bb6073e4c71642877b845b6eb42d1bd32398299daffbf", 1742766021019, 1.2817], "isolated vs django modes.peakmem_render_sm_first": [[44179456, 44175360], [["'isolated'", "'django'"]], "c51b91fc583295776062822225e720b5ed71aef9c9288217c401c54283c62840", 1742766022301, 0.37934], "isolated vs django modes.peakmem_render_sm_subsequent": [[44179456, 44179456], [["'isolated'", "'django'"]], "54d747fb8f40179b7ff3d2fc49eb195909ad1c880b5ef7b82f82742b27b67260", 1742766022681, 0.37941]}, "durations": {}, "version": 2} \ No newline at end of file diff --git a/.asv/results/ci-linux/4c909486-virtualenv-py3.13-django5.1-djc-core-html-parser.json b/.asv/results/ci-linux/4c909486-virtualenv-py3.13-django5.1-djc-core-html-parser.json new file mode 100644 index 00000000..3d570a90 --- /dev/null +++ b/.asv/results/ci-linux/4c909486-virtualenv-py3.13-django5.1-djc-core-html-parser.json @@ -0,0 +1 @@ +{"commit_hash": "4c909486069f3c3c8ee7915239174f820f081da4", "env_name": "virtualenv-py3.13-django5.1-djc-core-html-parser", "date": 1745143092000, "params": {"machine": "ci-linux", "python": "3.13", "django": "5.1", "djc-core-html-parser": ""}, "python": "3.13", "requirements": {"django": "5.1", "djc-core-html-parser": ""}, "env_vars": {}, "result_columns": ["result", "params", "version", "started_at", "duration", "stats_ci_99_a", "stats_ci_99_b", "stats_q_25", "stats_q_75", "stats_number", "stats_repeat", "samples", "profile"], "results": {"Components vs Django.timeraw_render_lg_first": [[0.07360306399999672, 0.2678246009999725], [["'django'", "'django-components'"]], "be3bf6236960046a028b6ea007aad28b2337fc2b906b8ce317a09a5d4f1a6193", 1745143577594, 32.042, [0.072955, 0.26442], [0.074281, 0.27014], [0.073086, 0.26445], [0.074028, 0.27007], [1, 1], [25, 25]], "Components vs Django.timeraw_render_lg_subsequent": [[0.037022983000014165, 0.15138703899998518], [["'django'", "'django-components'"]], "b98221c11a0ee6e9de0778d416d31b9dd514a674d9017a2bb9b2fc1cd0f01920", 1745143584022, 37.559, [0.036649, 0.14991], [0.037827, 0.1536], [0.036668, 0.1502], [0.037733, 0.15306], [1, 1], [25, 25]], "Components vs Django.timeraw_render_sm_first": [[0.003639607999986083, 0.004848561000017071], [["'django'", "'django-components'"]], "f1fc17e4a31c71f4d9265f1122da52e7cf57addb4dfa02606e303b33d6431b9b", 1745143591495, 17.115, [0.0036116, 0.0048089], [0.0036876, 0.0048932], [0.0036147, 0.0048094], [0.0036803, 0.0048807], [1, 1], [25, 25]], "Components vs Django.timeraw_render_sm_subsequent": [[0.00011665800002447213, 0.000582710000003317], [["'django'", "'django-components'"]], "6fce1cd85a9344fee383b40a22f27862120b9488a628420625592dc14e0307d3", 1745143594930, 17.167, [0.00011593, 0.00056917], [0.00011882, 0.0005887], [0.0001162, 0.00057013], [0.00011876, 0.00058637], [1, 1], [25, 25]], "Components vs Django.timeraw_startup_lg": [[0.224061646999985, 0.2246476189999953], [["'django'", "'django-components'"]], "53151821c128ad0ecfb0707fff3146e1abd8d0bcfa301aa056b5d3fae3d793e2", 1745143598346, 20.14, [0.22362, 0.22184], [0.22927, 0.22751], [0.22365, 0.22214], [0.22867, 0.22721], [1, 1], [25, 25]], "Other.timeraw_import_time": [[0.2053688209999791], [], "a0a1c1c0db22509410b946d0d4384b52ea4a09b47b6048d7d1cfb89b0c7fe5c3", 1745143602385, 8.1858, [0.20216], [0.20799], [0.20231], [0.20781], [1], [25]], "isolated vs django modes.timeraw_render_lg_first": [[0.2658582709999848, 0.2712929850000023], [["'isolated'", "'django'"]], "f94af83427c6346f88f8785a3cd2fc42415ac5a9fbbdb7de71d27e22e6a81699", 1745143607124, 37.445, [0.26292, 0.26717], [0.27104, 0.27439], [0.26301, 0.26741], [0.27007, 0.27412], [1, 1], [25, 25]], "isolated vs django modes.timeraw_render_lg_subsequent": [[0.15248822700002052, 0.15465820200000735], [["'isolated'", "'django'"]], "9f7c2fde6b33f0451a1794ed903c48d96cd7822f67da502cec36fe8e977c2414", 1745143614605, 46.778, [0.15022, 0.15322], [0.1536, 0.15739], [0.15038, 0.15326], [0.15356, 0.15661], [1, 1], [25, 25]], "isolated vs django modes.timeraw_render_sm_first": [[0.004847185000016907, 0.004857667999999649], [["'isolated'", "'django'"]], "d15ca68909d7f1f43ff16863befb6f42681f17461417fc0069eefd6db3569296", 1745143624026, 17.241, [0.004817, 0.0048044], [0.004914, 0.0049081], [0.0048254, 0.0048148], [0.004905, 0.004893], [1, 1], [25, 25]], "isolated vs django modes.timeraw_render_sm_subsequent": [[0.0005717709999828458, 0.0005785939999896073], [["'isolated'", "'django'"]], "7444bc9516dd087e3f420349345eae991ad6941bbd22fce45265b18034b7cf77", 1745143627490, 17.271, [0.00056467, 0.00056983], [0.00058564, 0.00058436], [0.00056901, 0.00057082], [0.00058274, 0.00058294], [1, 1], [25, 25]], "isolated vs django modes.timeraw_startup_lg": [[0.22378945699995256, 0.22211803700002974], [["'isolated'", "'django'"]], "eabe311ebee4a15c5816617be12f00ec30376f7506bd668219e1c50bc897c134", 1745143630975, 19.832, [0.22243, 0.22109], [0.22609, 0.224], [0.22277, 0.22114], [0.2254, 0.22377], [1, 1], [25, 25]], "Components vs Django.peakmem_render_lg_first": [[53153792, 55410688], [["'django'", "'django-components'"]], "301c396f017f45a5b3f71e85df58d15f54153fcfd951af7ef424641d4b31b528", 1745143575049, 0.80087], "Components vs Django.peakmem_render_lg_subsequent": [[53919744, 55799808], [["'django'", "'django-components'"]], "9a44e9999ef3ef42ea7e01323727490244febb43d66a87a4d8f88c6b8a133b8b", 1745143575851, 0.98861], "Components vs Django.peakmem_render_sm_first": [[44195840, 44453888], [["'django'", "'django-components'"]], "e93b7a5193681c883edf85bdb30b1bc0821263bf51033fdcee215b155085e036", 1745143576839, 0.37918], "Components vs Django.peakmem_render_sm_subsequent": [[44191744, 44453888], [["'django'", "'django-components'"]], "b46e0820b18950aa7cc5e61306ff3425b76b4da9dca42d64fae5b1d25c6c9026", 1745143577219, 0.37508], "isolated vs django modes.peakmem_render_lg_first": [[55382016, 54882304], [["'isolated'", "'django'"]], "c4bf0016d48d210f08b8db733b57c7dcba1cebbf548c458b93b86ace387067e9", 1745143604037, 0.99271], "isolated vs django modes.peakmem_render_lg_subsequent": [[55812096, 55902208], [["'isolated'", "'django'"]], "65bb1b8586487197a79bb6073e4c71642877b845b6eb42d1bd32398299daffbf", 1745143605030, 1.3237], "isolated vs django modes.peakmem_render_sm_first": [[44453888, 44453888], [["'isolated'", "'django'"]], "c51b91fc583295776062822225e720b5ed71aef9c9288217c401c54283c62840", 1745143606354, 0.38012], "isolated vs django modes.peakmem_render_sm_subsequent": [[44449792, 44449792], [["'isolated'", "'django'"]], "54d747fb8f40179b7ff3d2fc49eb195909ad1c880b5ef7b82f82742b27b67260", 1745143606735, 0.3888]}, "durations": {}, "version": 2} \ No newline at end of file diff --git a/.asv/results/ci-linux/5d7e2357-virtualenv-py3.13-django5.1-djc-core-html-parser.json b/.asv/results/ci-linux/5d7e2357-virtualenv-py3.13-django5.1-djc-core-html-parser.json new file mode 100644 index 00000000..5f591b2f --- /dev/null +++ b/.asv/results/ci-linux/5d7e2357-virtualenv-py3.13-django5.1-djc-core-html-parser.json @@ -0,0 +1 @@ +{"commit_hash": "5d7e235725449181f6b65b7bf97e43ea0e0f8552", "env_name": "virtualenv-py3.13-django5.1-djc-core-html-parser", "date": 1753049108000, "params": {"machine": "ci-linux", "python": "3.13", "django": "5.1", "djc-core-html-parser": ""}, "python": "3.13", "requirements": {"django": "5.1", "djc-core-html-parser": ""}, "env_vars": {}, "result_columns": ["result", "params", "version", "started_at", "duration", "stats_ci_99_a", "stats_ci_99_b", "stats_q_25", "stats_q_75", "stats_number", "stats_repeat", "samples", "profile"], "results": {"Components vs Django.timeraw_render_lg_first": [[0.08105427499998541, 0.29477426600001877], [["'django'", "'django-components'"]], "be3bf6236960046a028b6ea007aad28b2337fc2b906b8ce317a09a5d4f1a6193", 1753049851954, 33.615, [0.079961, 0.29197], [0.082291, 0.29933], [0.080014, 0.29202], [0.08217, 0.29917], [1, 1], [25, 25]], "Components vs Django.timeraw_render_lg_subsequent": [[0.043648402000002307, 0.17461173199995983], [["'django'", "'django-components'"]], "b98221c11a0ee6e9de0778d416d31b9dd514a674d9017a2bb9b2fc1cd0f01920", 1753049858567, 39.742, [0.043109, 0.17344], [0.044493, 0.17591], [0.043193, 0.17344], [0.044363, 0.17568], [1, 1], [25, 25]], "Components vs Django.timeraw_render_sm_first": [[0.0037106409999978496, 0.004899473999955717], [["'django'", "'django-components'"]], "f1fc17e4a31c71f4d9265f1122da52e7cf57addb4dfa02606e303b33d6431b9b", 1753049866520, 17.599, [0.0036469, 0.0048514], [0.003775, 0.0049767], [0.0036506, 0.0048584], [0.0037527, 0.0049267], [1, 1], [25, 25]], "Components vs Django.timeraw_render_sm_subsequent": [[0.00012706900002967814, 0.0006100459999629493], [["'django'", "'django-components'"]], "6fce1cd85a9344fee383b40a22f27862120b9488a628420625592dc14e0307d3", 1753049870045, 17.527, [0.00012441, 0.0005963], [0.00012848, 0.00063084], [0.00012455, 0.00059677], [0.00012835, 0.0006285], [1, 1], [25, 25]], "Components vs Django.timeraw_startup_lg": [[0.22799248500001568, 0.22723498599998493], [["'django'", "'django-components'"]], "53151821c128ad0ecfb0707fff3146e1abd8d0bcfa301aa056b5d3fae3d793e2", 1753049873538, 20.449, [0.22611, 0.22628], [0.22958, 0.22866], [0.22612, 0.2263], [0.22904, 0.22847], [1, 1], [25, 25]], "Other.timeraw_import_time": [[0.2056691309999792], [], "a0a1c1c0db22509410b946d0d4384b52ea4a09b47b6048d7d1cfb89b0c7fe5c3", 1753049877623, 8.3423, [0.20476], [0.20838], [0.20478], [0.20831], [1], [25]], "isolated vs django modes.timeraw_render_lg_first": [[0.2920349000000044, 0.2976166970000236], [["'isolated'", "'django'"]], "f94af83427c6346f88f8785a3cd2fc42415ac5a9fbbdb7de71d27e22e6a81699", 1753049882577, 39.421, [0.29051, 0.29507], [0.29462, 0.2996], [0.29057, 0.29561], [0.29428, 0.29935], [1, 1], [25, 25]], "isolated vs django modes.timeraw_render_lg_subsequent": [[0.17414895399997476, 0.178393189000019], [["'isolated'", "'django'"]], "9f7c2fde6b33f0451a1794ed903c48d96cd7822f67da502cec36fe8e977c2414", 1753049890450, 50.139, [0.17304, 0.17805], [0.17616, 0.17928], [0.17314, 0.17806], [0.17595, 0.17923], [1, 1], [25, 25]], "isolated vs django modes.timeraw_render_sm_first": [[0.004897051999989799, 0.004863266000029398], [["'isolated'", "'django'"]], "d15ca68909d7f1f43ff16863befb6f42681f17461417fc0069eefd6db3569296", 1753049900457, 17.558, [0.0048512, 0.004838], [0.0049693, 0.0049278], [0.0048517, 0.0048435], [0.0049605, 0.0049223], [1, 1], [25, 25]], "isolated vs django modes.timeraw_render_sm_subsequent": [[0.0006159270000125616, 0.0006080119999865019], [["'isolated'", "'django'"]], "7444bc9516dd087e3f420349345eae991ad6941bbd22fce45265b18034b7cf77", 1753049903955, 17.521, [0.00060693, 0.0005956], [0.00062901, 0.00061672], [0.00060764, 0.00059691], [0.00062683, 0.00061634], [1, 1], [25, 25]], "isolated vs django modes.timeraw_startup_lg": [[0.22777395400004252, 0.2292747939999913], [["'isolated'", "'django'"]], "eabe311ebee4a15c5816617be12f00ec30376f7506bd668219e1c50bc897c134", 1753049907456, 20.282, [0.22683, 0.22736], [0.2295, 0.23339], [0.22692, 0.22764], [0.22924, 0.23178], [1, 1], [25, 25]], "Components vs Django.peakmem_render_lg_first": [[52715520, 56090624], [["'django'", "'django-components'"]], "301c396f017f45a5b3f71e85df58d15f54153fcfd951af7ef424641d4b31b528", 1753049849303, 0.82982], "Components vs Django.peakmem_render_lg_subsequent": [[52736000, 56791040], [["'django'", "'django-components'"]], "9a44e9999ef3ef42ea7e01323727490244febb43d66a87a4d8f88c6b8a133b8b", 1753049850134, 1.0513], "Components vs Django.peakmem_render_sm_first": [[44871680, 44912640], [["'django'", "'django-components'"]], "e93b7a5193681c883edf85bdb30b1bc0821263bf51033fdcee215b155085e036", 1753049851185, 0.38233], "Components vs Django.peakmem_render_sm_subsequent": [[44617728, 44986368], [["'django'", "'django-components'"]], "b46e0820b18950aa7cc5e61306ff3425b76b4da9dca42d64fae5b1d25c6c9026", 1753049851568, 0.38596], "isolated vs django modes.peakmem_render_lg_first": [[56090624, 55582720], [["'isolated'", "'django'"]], "c4bf0016d48d210f08b8db733b57c7dcba1cebbf548c458b93b86ace387067e9", 1753049879302, 1.0575], "isolated vs django modes.peakmem_render_lg_subsequent": [[56786944, 56778752], [["'isolated'", "'django'"]], "65bb1b8586487197a79bb6073e4c71642877b845b6eb42d1bd32398299daffbf", 1753049880360, 1.4396], "isolated vs django modes.peakmem_render_sm_first": [[44843008, 44851200], [["'isolated'", "'django'"]], "c51b91fc583295776062822225e720b5ed71aef9c9288217c401c54283c62840", 1753049881800, 0.38828], "isolated vs django modes.peakmem_render_sm_subsequent": [[44982272, 44986368], [["'isolated'", "'django'"]], "54d747fb8f40179b7ff3d2fc49eb195909ad1c880b5ef7b82f82742b27b67260", 1753049882189, 0.38775]}, "durations": {}, "version": 2} \ No newline at end of file diff --git a/.asv/results/ci-linux/7b24b86f-virtualenv-py3.13-django5.1-djc-core-html-parser.json b/.asv/results/ci-linux/7b24b86f-virtualenv-py3.13-django5.1-djc-core-html-parser.json new file mode 100644 index 00000000..3acec4f5 --- /dev/null +++ b/.asv/results/ci-linux/7b24b86f-virtualenv-py3.13-django5.1-djc-core-html-parser.json @@ -0,0 +1 @@ +{"commit_hash": "7b24b86f4a836c697acba926d9d6602afa45418d", "env_name": "virtualenv-py3.13-django5.1-djc-core-html-parser", "date": 1749076521000, "params": {"machine": "ci-linux", "python": "3.13", "django": "5.1", "djc-core-html-parser": ""}, "python": "3.13", "requirements": {"django": "5.1", "djc-core-html-parser": ""}, "env_vars": {}, "result_columns": ["result", "params", "version", "started_at", "duration", "stats_ci_99_a", "stats_ci_99_b", "stats_q_25", "stats_q_75", "stats_number", "stats_repeat", "samples", "profile"], "results": {"Components vs Django.timeraw_render_lg_first": [[0.07941284200001064, 0.26779402600004687], [["'django'", "'django-components'"]], "be3bf6236960046a028b6ea007aad28b2337fc2b906b8ce317a09a5d4f1a6193", 1749076875010, 33.29, [0.078956, 0.26662], [0.081186, 0.27149], [0.079001, 0.26705], [0.080936, 0.27135], [1, 1], [25, 25]], "Components vs Django.timeraw_render_lg_subsequent": [[0.043317416999911984, 0.15457556900003055], [["'django'", "'django-components'"]], "b98221c11a0ee6e9de0778d416d31b9dd514a674d9017a2bb9b2fc1cd0f01920", 1749076881523, 38.751, [0.042862, 0.15282], [0.044278, 0.15833], [0.042896, 0.15289], [0.044184, 0.15671], [1, 1], [25, 25]], "Components vs Django.timeraw_render_sm_first": [[0.0036632869999948525, 0.00493345400002454], [["'django'", "'django-components'"]], "f1fc17e4a31c71f4d9265f1122da52e7cf57addb4dfa02606e303b33d6431b9b", 1749076889315, 17.497, [0.0036441, 0.0048964], [0.0037017, 0.0050018], [0.0036475, 0.0049028], [0.0037007, 0.0049814], [1, 1], [25, 25]], "Components vs Django.timeraw_render_sm_subsequent": [[0.00012153600005149201, 0.0005999570000199128], [["'django'", "'django-components'"]], "6fce1cd85a9344fee383b40a22f27862120b9488a628420625592dc14e0307d3", 1749076892858, 17.52, [0.00012055, 0.00057981], [0.00012289, 0.00061584], [0.00012056, 0.00058243], [0.00012282, 0.00061301], [1, 1], [25, 25]], "Components vs Django.timeraw_startup_lg": [[0.22743783699991127, 0.226070988999993], [["'django'", "'django-components'"]], "53151821c128ad0ecfb0707fff3146e1abd8d0bcfa301aa056b5d3fae3d793e2", 1749076896345, 20.618, [0.22567, 0.22411], [0.22912, 0.22959], [0.22581, 0.22418], [0.22858, 0.22815], [1, 1], [25, 25]], "Other.timeraw_import_time": [[0.2063091950000171], [], "a0a1c1c0db22509410b946d0d4384b52ea4a09b47b6048d7d1cfb89b0c7fe5c3", 1749076900468, 8.3563, [0.20413], [0.20858], [0.20424], [0.20774], [1], [25]], "isolated vs django modes.timeraw_render_lg_first": [[0.2675778039999841, 0.2724974679999832], [["'isolated'", "'django'"]], "f94af83427c6346f88f8785a3cd2fc42415ac5a9fbbdb7de71d27e22e6a81699", 1749076905243, 38.085, [0.26574, 0.27164], [0.27197, 0.27673], [0.26585, 0.27165], [0.27065, 0.27581], [1, 1], [25, 25]], "isolated vs django modes.timeraw_render_lg_subsequent": [[0.15459265900005903, 0.15926110200007315], [["'isolated'", "'django'"]], "9f7c2fde6b33f0451a1794ed903c48d96cd7822f67da502cec36fe8e977c2414", 1749076912924, 47.983, [0.15333, 0.15793], [0.15993, 0.16206], [0.1535, 0.15845], [0.15979, 0.16152], [1, 1], [25, 25]], "isolated vs django modes.timeraw_render_sm_first": [[0.004923484000073586, 0.004925836999973399], [["'isolated'", "'django'"]], "d15ca68909d7f1f43ff16863befb6f42681f17461417fc0069eefd6db3569296", 1749076922561, 17.577, [0.0048564, 0.0048656], [0.0050014, 0.004982], [0.0048761, 0.0048661], [0.004997, 0.004977], [1, 1], [25, 25]], "isolated vs django modes.timeraw_render_sm_subsequent": [[0.0005969709999362749, 0.0005864510000037626], [["'isolated'", "'django'"]], "7444bc9516dd087e3f420349345eae991ad6941bbd22fce45265b18034b7cf77", 1749076926091, 17.606, [0.00058144, 0.00058324], [0.00060954, 0.00059625], [0.00058749, 0.00058422], [0.00060768, 0.00059614], [1, 1], [25, 25]], "isolated vs django modes.timeraw_startup_lg": [[0.22545313400001987, 0.22602228000005198], [["'isolated'", "'django'"]], "eabe311ebee4a15c5816617be12f00ec30376f7506bd668219e1c50bc897c134", 1749076929634, 20.393, [0.22445, 0.22472], [0.22834, 0.22927], [0.2245, 0.22475], [0.22781, 0.22878], [1, 1], [25, 25]], "Components vs Django.peakmem_render_lg_first": [[52957184, 55177216], [["'django'", "'django-components'"]], "301c396f017f45a5b3f71e85df58d15f54153fcfd951af7ef424641d4b31b528", 1749076872421, 0.81152], "Components vs Django.peakmem_render_lg_subsequent": [[52822016, 56242176], [["'django'", "'django-components'"]], "9a44e9999ef3ef42ea7e01323727490244febb43d66a87a4d8f88c6b8a133b8b", 1749076873233, 1.0095], "Components vs Django.peakmem_render_sm_first": [[44756992, 44744704], [["'django'", "'django-components'"]], "e93b7a5193681c883edf85bdb30b1bc0821263bf51033fdcee215b155085e036", 1749076874243, 0.37927], "Components vs Django.peakmem_render_sm_subsequent": [[44527616, 44744704], [["'django'", "'django-components'"]], "b46e0820b18950aa7cc5e61306ff3425b76b4da9dca42d64fae5b1d25c6c9026", 1749076874622, 0.38717], "isolated vs django modes.peakmem_render_lg_first": [[55222272, 54722560], [["'isolated'", "'django'"]], "c4bf0016d48d210f08b8db733b57c7dcba1cebbf548c458b93b86ace387067e9", 1749076902121, 1.0091], "isolated vs django modes.peakmem_render_lg_subsequent": [[56008704, 56143872], [["'isolated'", "'django'"]], "65bb1b8586487197a79bb6073e4c71642877b845b6eb42d1bd32398299daffbf", 1749076903130, 1.3368], "isolated vs django modes.peakmem_render_sm_first": [[44744704, 44744704], [["'isolated'", "'django'"]], "c51b91fc583295776062822225e720b5ed71aef9c9288217c401c54283c62840", 1749076904467, 0.38687], "isolated vs django modes.peakmem_render_sm_subsequent": [[44744704, 44744704], [["'isolated'", "'django'"]], "54d747fb8f40179b7ff3d2fc49eb195909ad1c880b5ef7b82f82742b27b67260", 1749076904855, 0.38848]}, "durations": {}, "version": 2} \ No newline at end of file diff --git a/.asv/results/ci-linux/a6455d70-virtualenv-py3.13-django5.1-djc-core-html-parser.json b/.asv/results/ci-linux/a6455d70-virtualenv-py3.13-django5.1-djc-core-html-parser.json new file mode 100644 index 00000000..9c06c27b --- /dev/null +++ b/.asv/results/ci-linux/a6455d70-virtualenv-py3.13-django5.1-djc-core-html-parser.json @@ -0,0 +1 @@ +{"commit_hash": "a6455d70f6c28ddbd4be8e58902f6cbc101e5ff3", "env_name": "virtualenv-py3.13-django5.1-djc-core-html-parser", "date": 1743430242000, "params": {"machine": "ci-linux", "python": "3.13", "django": "5.1", "djc-core-html-parser": ""}, "python": "3.13", "requirements": {"django": "5.1", "djc-core-html-parser": ""}, "env_vars": {}, "result_columns": ["result", "params", "version", "started_at", "duration", "stats_ci_99_a", "stats_ci_99_b", "stats_q_25", "stats_q_75", "stats_number", "stats_repeat", "samples", "profile"], "results": {"Components vs Django.timeraw_render_lg_first": [[0.07402671400001282, 0.26584690599997884], [["'django'", "'django-components'"]], "be3bf6236960046a028b6ea007aad28b2337fc2b906b8ce317a09a5d4f1a6193", 1743430606684, 32.442, [0.072957, 0.26285], [0.075615, 0.26888], [0.073023, 0.26377], [0.07513, 0.26834], [1, 1], [25, 25]], "Components vs Django.timeraw_render_lg_subsequent": [[0.03742426899998463, 0.14901454800002512], [["'django'", "'django-components'"]], "b98221c11a0ee6e9de0778d416d31b9dd514a674d9017a2bb9b2fc1cd0f01920", 1743430613250, 37.315, [0.036847, 0.14808], [0.037742, 0.15105], [0.036915, 0.14816], [0.037683, 0.15098], [1, 1], [25, 25]], "Components vs Django.timeraw_render_sm_first": [[0.003602947999979733, 0.004853936999950292], [["'django'", "'django-components'"]], "f1fc17e4a31c71f4d9265f1122da52e7cf57addb4dfa02606e303b33d6431b9b", 1743430620772, 16.954, [0.0035642, 0.0047703], [0.0036621, 0.0048984], [0.0035757, 0.0047716], [0.0036581, 0.0048887], [1, 1], [25, 25]], "Components vs Django.timeraw_render_sm_subsequent": [[0.00012266099997759738, 0.0005711430000019391], [["'django'", "'django-components'"]], "6fce1cd85a9344fee383b40a22f27862120b9488a628420625592dc14e0307d3", 1743430624325, 16.779, [0.00012174, 0.00055344], [0.0001259, 0.00058069], [0.00012179, 0.00055434], [0.0001247, 0.00057942], [1, 1], [25, 25]], "Components vs Django.timeraw_startup_lg": [[0.22133603999998286, 0.21805855799999563], [["'django'", "'django-components'"]], "53151821c128ad0ecfb0707fff3146e1abd8d0bcfa301aa056b5d3fae3d793e2", 1743430627696, 19.918, [0.21912, 0.21676], [0.2249, 0.22143], [0.21935, 0.21703], [0.22467, 0.22078], [1, 1], [25, 25]], "Other.timeraw_import_time": [[0.19950735400001918], [], "a0a1c1c0db22509410b946d0d4384b52ea4a09b47b6048d7d1cfb89b0c7fe5c3", 1743430631679, 8.1122, [0.19829], [0.20456], [0.1984], [0.20447], [1], [25]], "isolated vs django modes.timeraw_render_lg_first": [[0.2646600410000133, 0.2676605120000204], [["'isolated'", "'django'"]], "f94af83427c6346f88f8785a3cd2fc42415ac5a9fbbdb7de71d27e22e6a81699", 1743430636362, 37.078, [0.26139, 0.26374], [0.26862, 0.27072], [0.26167, 0.26378], [0.26789, 0.27042], [1, 1], [25, 25]], "isolated vs django modes.timeraw_render_lg_subsequent": [[0.14860135300000366, 0.15305296299999327], [["'isolated'", "'django'"]], "9f7c2fde6b33f0451a1794ed903c48d96cd7822f67da502cec36fe8e977c2414", 1743430643868, 46.445, [0.1469, 0.15166], [0.15298, 0.15727], [0.1471, 0.15188], [0.15174, 0.15702], [1, 1], [25, 25]], "isolated vs django modes.timeraw_render_sm_first": [[0.0048215560000244295, 0.004858458999990489], [["'isolated'", "'django'"]], "d15ca68909d7f1f43ff16863befb6f42681f17461417fc0069eefd6db3569296", 1743430653175, 17.045, [0.0047768, 0.0047682], [0.0049063, 0.0049223], [0.0047773, 0.0047722], [0.0048597, 0.0049158], [1, 1], [25, 25]], "isolated vs django modes.timeraw_render_sm_subsequent": [[0.0005736080000247057, 0.0005720849999875099], [["'isolated'", "'django'"]], "7444bc9516dd087e3f420349345eae991ad6941bbd22fce45265b18034b7cf77", 1743430656658, 17.201, [0.00055793, 0.00056215], [0.00059018, 0.0005847], [0.00056001, 0.00056312], [0.00058173, 0.00057813], [1, 1], [25, 25]], "isolated vs django modes.timeraw_startup_lg": [[0.2199001029999863, 0.22046102699999892], [["'isolated'", "'django'"]], "eabe311ebee4a15c5816617be12f00ec30376f7506bd668219e1c50bc897c134", 1743430660197, 19.832, [0.21722, 0.21683], [0.22316, 0.225], [0.21767, 0.21732], [0.22306, 0.22354], [1, 1], [25, 25]], "Components vs Django.peakmem_render_lg_first": [[52936704, 55009280], [["'django'", "'django-components'"]], "301c396f017f45a5b3f71e85df58d15f54153fcfd951af7ef424641d4b31b528", 1743430604182, 0.78721], "Components vs Django.peakmem_render_lg_subsequent": [[53768192, 55455744], [["'django'", "'django-components'"]], "9a44e9999ef3ef42ea7e01323727490244febb43d66a87a4d8f88c6b8a133b8b", 1743430604969, 0.97304], "Components vs Django.peakmem_render_sm_first": [[44191744, 44314624], [["'django'", "'django-components'"]], "e93b7a5193681c883edf85bdb30b1bc0821263bf51033fdcee215b155085e036", 1743430605943, 0.36867], "Components vs Django.peakmem_render_sm_subsequent": [[44191744, 44437504], [["'django'", "'django-components'"]], "b46e0820b18950aa7cc5e61306ff3425b76b4da9dca42d64fae5b1d25c6c9026", 1743430606312, 0.37196], "isolated vs django modes.peakmem_render_lg_first": [[55001088, 54312960], [["'isolated'", "'django'"]], "c4bf0016d48d210f08b8db733b57c7dcba1cebbf548c458b93b86ace387067e9", 1743430633331, 0.98374], "isolated vs django modes.peakmem_render_lg_subsequent": [[55439360, 55369728], [["'isolated'", "'django'"]], "65bb1b8586487197a79bb6073e4c71642877b845b6eb42d1bd32398299daffbf", 1743430634315, 1.2833], "isolated vs django modes.peakmem_render_sm_first": [[44314624, 44310528], [["'isolated'", "'django'"]], "c51b91fc583295776062822225e720b5ed71aef9c9288217c401c54283c62840", 1743430635599, 0.37624], "isolated vs django modes.peakmem_render_sm_subsequent": [[44310528, 44314624], [["'isolated'", "'django'"]], "54d747fb8f40179b7ff3d2fc49eb195909ad1c880b5ef7b82f82742b27b67260", 1743430635975, 0.38685]}, "durations": {}, "version": 2} \ No newline at end of file diff --git a/.asv/results/ci-linux/ad402fc6-virtualenv-py3.13-django5.1-djc-core-html-parser.json b/.asv/results/ci-linux/ad402fc6-virtualenv-py3.13-django5.1-djc-core-html-parser.json new file mode 100644 index 00000000..426b7f59 --- /dev/null +++ b/.asv/results/ci-linux/ad402fc6-virtualenv-py3.13-django5.1-djc-core-html-parser.json @@ -0,0 +1 @@ +{"commit_hash": "ad402fc619922b6d2edf1e99b7082d2a58632076", "env_name": "virtualenv-py3.13-django5.1-djc-core-html-parser", "date": 1744443333000, "params": {"machine": "ci-linux", "python": "3.13", "django": "5.1", "djc-core-html-parser": ""}, "python": "3.13", "requirements": {"django": "5.1", "djc-core-html-parser": ""}, "env_vars": {}, "result_columns": ["result", "params", "version", "started_at", "duration", "stats_ci_99_a", "stats_ci_99_b", "stats_q_25", "stats_q_75", "stats_number", "stats_repeat", "samples", "profile"], "results": {"Components vs Django.timeraw_render_lg_first": [[0.07303507899999317, 0.2628890319999755], [["'django'", "'django-components'"]], "be3bf6236960046a028b6ea007aad28b2337fc2b906b8ce317a09a5d4f1a6193", 1744443661806, 31.899, [0.072617, 0.26159], [0.074239, 0.26702], [0.072622, 0.26164], [0.073884, 0.26623], [1, 1], [25, 25]], "Components vs Django.timeraw_render_lg_subsequent": [[0.03678920999999491, 0.14955294699998944], [["'django'", "'django-components'"]], "b98221c11a0ee6e9de0778d416d31b9dd514a674d9017a2bb9b2fc1cd0f01920", 1744443668113, 37.058, [0.036485, 0.14786], [0.037805, 0.15173], [0.036589, 0.14859], [0.037797, 0.15139], [1, 1], [25, 25]], "Components vs Django.timeraw_render_sm_first": [[0.0035613420000117912, 0.004760385999986738], [["'django'", "'django-components'"]], "f1fc17e4a31c71f4d9265f1122da52e7cf57addb4dfa02606e303b33d6431b9b", 1744443675539, 16.816, [0.0035318, 0.0047156], [0.0035817, 0.0047979], [0.0035329, 0.004732], [0.0035726, 0.0047786], [1, 1], [25, 25]], "Components vs Django.timeraw_render_sm_subsequent": [[0.00011622699999236374, 0.0005842630000074678], [["'django'", "'django-components'"]], "6fce1cd85a9344fee383b40a22f27862120b9488a628420625592dc14e0307d3", 1744443678895, 16.88, [0.00011473, 0.00056683], [0.00011709, 0.00060847], [0.00011487, 0.0005685], [0.00011689, 0.00059169], [1, 1], [25, 25]], "Components vs Django.timeraw_startup_lg": [[0.22073260000001937, 0.2182690520000392], [["'django'", "'django-components'"]], "53151821c128ad0ecfb0707fff3146e1abd8d0bcfa301aa056b5d3fae3d793e2", 1744443682320, 19.801, [0.21972, 0.21695], [0.22309, 0.22228], [0.22028, 0.21699], [0.22296, 0.22213], [1, 1], [25, 25]], "Other.timeraw_import_time": [[0.19919827600000417], [], "a0a1c1c0db22509410b946d0d4384b52ea4a09b47b6048d7d1cfb89b0c7fe5c3", 1744443686264, 8.0021, [0.19862], [0.20007], [0.19866], [0.19998], [1], [25]], "isolated vs django modes.timeraw_render_lg_first": [[0.2626667089999728, 0.2663110299999971], [["'isolated'", "'django'"]], "f94af83427c6346f88f8785a3cd2fc42415ac5a9fbbdb7de71d27e22e6a81699", 1744443690855, 37.02, [0.26033, 0.2649], [0.26828, 0.27124], [0.26038, 0.26525], [0.26827, 0.27069], [1, 1], [25, 25]], "isolated vs django modes.timeraw_render_lg_subsequent": [[0.14876902899999322, 0.15549233400000162], [["'isolated'", "'django'"]], "9f7c2fde6b33f0451a1794ed903c48d96cd7822f67da502cec36fe8e977c2414", 1744443698232, 46.228, [0.1479, 0.15374], [0.15179, 0.15735], [0.14799, 0.15375], [0.15155, 0.15701], [1, 1], [25, 25]], "isolated vs django modes.timeraw_render_sm_first": [[0.00480728600001612, 0.00472804499997892], [["'isolated'", "'django'"]], "d15ca68909d7f1f43ff16863befb6f42681f17461417fc0069eefd6db3569296", 1744443707554, 16.94, [0.0047171, 0.0047064], [0.0048771, 0.004802], [0.0047325, 0.0047131], [0.0048683, 0.0047952], [1, 1], [25, 25]], "isolated vs django modes.timeraw_render_sm_subsequent": [[0.0005810670000130358, 0.000576186999978745], [["'isolated'", "'django'"]], "7444bc9516dd087e3f420349345eae991ad6941bbd22fce45265b18034b7cf77", 1744443710989, 17.038, [0.00056804, 0.00056565], [0.00059006, 0.00058984], [0.00057126, 0.00056676], [0.00058908, 0.0005876], [1, 1], [25, 25]], "isolated vs django modes.timeraw_startup_lg": [[0.21867883100003382, 0.21859779499999377], [["'isolated'", "'django'"]], "eabe311ebee4a15c5816617be12f00ec30376f7506bd668219e1c50bc897c134", 1744443714408, 19.48, [0.21759, 0.21774], [0.22135, 0.22052], [0.21759, 0.21791], [0.22105, 0.22031], [1, 1], [25, 25]], "Components vs Django.peakmem_render_lg_first": [[52490240, 55361536], [["'django'", "'django-components'"]], "301c396f017f45a5b3f71e85df58d15f54153fcfd951af7ef424641d4b31b528", 1744443659333, 0.7721], "Components vs Django.peakmem_render_lg_subsequent": [[52097024, 55791616], [["'django'", "'django-components'"]], "9a44e9999ef3ef42ea7e01323727490244febb43d66a87a4d8f88c6b8a133b8b", 1744443660105, 0.97576], "Components vs Django.peakmem_render_sm_first": [[44183552, 44306432], [["'django'", "'django-components'"]], "e93b7a5193681c883edf85bdb30b1bc0821263bf51033fdcee215b155085e036", 1744443661081, 0.36008], "Components vs Django.peakmem_render_sm_subsequent": [[44314624, 44437504], [["'django'", "'django-components'"]], "b46e0820b18950aa7cc5e61306ff3425b76b4da9dca42d64fae5b1d25c6c9026", 1744443661442, 0.3642], "isolated vs django modes.peakmem_render_lg_first": [[55357440, 54874112], [["'isolated'", "'django'"]], "c4bf0016d48d210f08b8db733b57c7dcba1cebbf548c458b93b86ace387067e9", 1744443687861, 0.9722], "isolated vs django modes.peakmem_render_lg_subsequent": [[55640064, 55631872], [["'isolated'", "'django'"]], "65bb1b8586487197a79bb6073e4c71642877b845b6eb42d1bd32398299daffbf", 1744443688833, 1.2825], "isolated vs django modes.peakmem_render_sm_first": [[44306432, 44240896], [["'isolated'", "'django'"]], "c51b91fc583295776062822225e720b5ed71aef9c9288217c401c54283c62840", 1744443690116, 0.37071], "isolated vs django modes.peakmem_render_sm_subsequent": [[44437504, 44437504], [["'isolated'", "'django'"]], "54d747fb8f40179b7ff3d2fc49eb195909ad1c880b5ef7b82f82742b27b67260", 1744443690487, 0.36747]}, "durations": {}, "version": 2} \ No newline at end of file diff --git a/.asv/results/ci-linux/c692b7a3-virtualenv-py3.13-django5.1-djc-core-html-parser.json b/.asv/results/ci-linux/c692b7a3-virtualenv-py3.13-django5.1-djc-core-html-parser.json new file mode 100644 index 00000000..14392b99 --- /dev/null +++ b/.asv/results/ci-linux/c692b7a3-virtualenv-py3.13-django5.1-djc-core-html-parser.json @@ -0,0 +1 @@ +{"commit_hash": "c692b7a3105c65414d2c23c357ffed9debdbf6e9", "env_name": "virtualenv-py3.13-django5.1-djc-core-html-parser", "date": 1751538441000, "params": {"machine": "ci-linux", "python": "3.13", "django": "5.1", "djc-core-html-parser": ""}, "python": "3.13", "requirements": {"django": "5.1", "djc-core-html-parser": ""}, "env_vars": {}, "result_columns": ["result", "params", "version", "started_at", "duration", "stats_ci_99_a", "stats_ci_99_b", "stats_q_25", "stats_q_75", "stats_number", "stats_repeat", "samples", "profile"], "results": {"Components vs Django.timeraw_render_lg_first": [[0.0814841690000776, 0.28364495499999975], [["'django'", "'django-components'"]], "be3bf6236960046a028b6ea007aad28b2337fc2b906b8ce317a09a5d4f1a6193", 1751538832752, 34.343, [0.079655, 0.2763], [0.083151, 0.28972], [0.079684, 0.27776], [0.082952, 0.28852], [1, 1], [25, 25]], "Components vs Django.timeraw_render_lg_subsequent": [[0.04362213900003553, 0.16551773399999092], [["'django'", "'django-components'"]], "b98221c11a0ee6e9de0778d416d31b9dd514a674d9017a2bb9b2fc1cd0f01920", 1751538839695, 39.947, [0.042835, 0.16259], [0.044314, 0.1709], [0.04289, 0.16268], [0.044297, 0.17072], [1, 1], [25, 25]], "Components vs Django.timeraw_render_sm_first": [[0.00375721499995052, 0.0049729269999261305], [["'django'", "'django-components'"]], "f1fc17e4a31c71f4d9265f1122da52e7cf57addb4dfa02606e303b33d6431b9b", 1751538847853, 18.382, [0.0036459, 0.0048221], [0.0038196, 0.0051737], [0.0036541, 0.0048489], [0.0038098, 0.0051177], [1, 1], [25, 25]], "Components vs Django.timeraw_render_sm_subsequent": [[0.00012686900004155177, 0.0006182140000419167], [["'django'", "'django-components'"]], "6fce1cd85a9344fee383b40a22f27862120b9488a628420625592dc14e0307d3", 1751538851498, 18.239, [0.00012524, 0.00060676], [0.00013161, 0.0006405], [0.00012579, 0.00060829], [0.00012952, 0.00063066], [1, 1], [25, 25]], "Components vs Django.timeraw_startup_lg": [[0.23076480500003527, 0.23163660399995933], [["'django'", "'django-components'"]], "53151821c128ad0ecfb0707fff3146e1abd8d0bcfa301aa056b5d3fae3d793e2", 1751538855102, 21.331, [0.228, 0.22934], [0.24129, 0.23958], [0.22861, 0.23032], [0.23462, 0.23903], [1, 1], [25, 25]], "Other.timeraw_import_time": [[0.21042045099989082], [], "a0a1c1c0db22509410b946d0d4384b52ea4a09b47b6048d7d1cfb89b0c7fe5c3", 1751538859357, 8.7502, [0.20296], [0.21809], [0.20498], [0.21758], [1], [25]], "isolated vs django modes.timeraw_render_lg_first": [[0.2794132599999557, 0.28440619299999526], [["'isolated'", "'django'"]], "f94af83427c6346f88f8785a3cd2fc42415ac5a9fbbdb7de71d27e22e6a81699", 1751538864363, 39.572, [0.27575, 0.28135], [0.28414, 0.29307], [0.2763, 0.28173], [0.2821, 0.29138], [1, 1], [25, 25]], "isolated vs django modes.timeraw_render_lg_subsequent": [[0.16650312799993117, 0.17177308600003016], [["'isolated'", "'django'"]], "9f7c2fde6b33f0451a1794ed903c48d96cd7822f67da502cec36fe8e977c2414", 1751538872327, 50.556, [0.1622, 0.16821], [0.16877, 0.17739], [0.16257, 0.16909], [0.16869, 0.17637], [1, 1], [25, 25]], "isolated vs django modes.timeraw_render_sm_first": [[0.005049280000093859, 0.004947880000145233], [["'isolated'", "'django'"]], "d15ca68909d7f1f43ff16863befb6f42681f17461417fc0069eefd6db3569296", 1751538882427, 18.336, [0.0048311, 0.0047979], [0.0051732, 0.0051086], [0.0048842, 0.0047995], [0.005169, 0.0050803], [1, 1], [25, 25]], "isolated vs django modes.timeraw_render_sm_subsequent": [[0.0006160310000495883, 0.0006166809999967882], [["'isolated'", "'django'"]], "7444bc9516dd087e3f420349345eae991ad6941bbd22fce45265b18034b7cf77", 1751538886046, 18.232, [0.00060088, 0.00060389], [0.00063506, 0.00063793], [0.00060598, 0.00060406], [0.00063334, 0.00063127], [1, 1], [25, 25]], "isolated vs django modes.timeraw_startup_lg": [[0.2295973340000046, 0.23030742100002044], [["'isolated'", "'django'"]], "eabe311ebee4a15c5816617be12f00ec30376f7506bd668219e1c50bc897c134", 1751538889806, 21.076, [0.22658, 0.22629], [0.24075, 0.2421], [0.22709, 0.22647], [0.23956, 0.2393], [1, 1], [25, 25]], "Components vs Django.peakmem_render_lg_first": [[53096448, 55484416], [["'django'", "'django-components'"]], "301c396f017f45a5b3f71e85df58d15f54153fcfd951af7ef424641d4b31b528", 1751538830129, 0.81232], "Components vs Django.peakmem_render_lg_subsequent": [[53018624, 56389632], [["'django'", "'django-components'"]], "9a44e9999ef3ef42ea7e01323727490244febb43d66a87a4d8f88c6b8a133b8b", 1751538830942, 1.0287], "Components vs Django.peakmem_render_sm_first": [[44716032, 44969984], [["'django'", "'django-components'"]], "e93b7a5193681c883edf85bdb30b1bc0821263bf51033fdcee215b155085e036", 1751538831971, 0.39208], "Components vs Django.peakmem_render_sm_subsequent": [[44724224, 44969984], [["'django'", "'django-components'"]], "b46e0820b18950aa7cc5e61306ff3425b76b4da9dca42d64fae5b1d25c6c9026", 1751538832363, 0.38781], "isolated vs django modes.peakmem_render_lg_first": [[55476224, 54968320], [["'isolated'", "'django'"]], "c4bf0016d48d210f08b8db733b57c7dcba1cebbf548c458b93b86ace387067e9", 1751538861133, 1.051], "isolated vs django modes.peakmem_render_lg_subsequent": [[56352768, 56516608], [["'isolated'", "'django'"]], "65bb1b8586487197a79bb6073e4c71642877b845b6eb42d1bd32398299daffbf", 1751538862185, 1.3815], "isolated vs django modes.peakmem_render_sm_first": [[44969984, 44969984], [["'isolated'", "'django'"]], "c51b91fc583295776062822225e720b5ed71aef9c9288217c401c54283c62840", 1751538863566, 0.38739], "isolated vs django modes.peakmem_render_sm_subsequent": [[44974080, 44974080], [["'isolated'", "'django'"]], "54d747fb8f40179b7ff3d2fc49eb195909ad1c880b5ef7b82f82742b27b67260", 1751538863954, 0.40929]}, "durations": {}, "version": 2} \ No newline at end of file diff --git a/.asv/results/ci-linux/d0a42a26-virtualenv-py3.13-django5.1-djc-core-html-parser.json b/.asv/results/ci-linux/d0a42a26-virtualenv-py3.13-django5.1-djc-core-html-parser.json new file mode 100644 index 00000000..ca3e5fe8 --- /dev/null +++ b/.asv/results/ci-linux/d0a42a26-virtualenv-py3.13-django5.1-djc-core-html-parser.json @@ -0,0 +1 @@ +{"commit_hash": "d0a42a2698f2ba21e7ab2dec750c5dbadeda0db5", "env_name": "virtualenv-py3.13-django5.1-djc-core-html-parser", "date": 1742502414000, "params": {"machine": "ci-linux", "python": "3.13", "django": "5.1", "djc-core-html-parser": ""}, "python": "3.13", "requirements": {"django": "5.1", "djc-core-html-parser": ""}, "env_vars": {}, "result_columns": ["result", "params", "version", "started_at", "duration", "stats_ci_99_a", "stats_ci_99_b", "stats_q_25", "stats_q_75", "stats_number", "stats_repeat", "samples", "profile"], "results": {"Components vs Django.timeraw_render_lg_first": [[0.06960565700001098, 0.25608221199996706], [["'django'", "'django-components'"]], "be3bf6236960046a028b6ea007aad28b2337fc2b906b8ce317a09a5d4f1a6193", 1742503651927, 30.962, [0.069111, 0.25384], [0.071024, 0.25982], [0.069155, 0.25418], [0.07084, 0.25904], [1, 1], [25, 25]], "Components vs Django.timeraw_render_lg_subsequent": [[0.03327357099999517, 0.1421111020000012], [["'django'", "'django-components'"]], "b98221c11a0ee6e9de0778d416d31b9dd514a674d9017a2bb9b2fc1cd0f01920", 1742503658029, 35.986, [0.032901, 0.14037], [0.03367, 0.14348], [0.033021, 0.14072], [0.033649, 0.14344], [1, 1], [25, 25]], "Components vs Django.timeraw_render_sm_first": [[0.0035443229999998493, 0.00467639600003622], [["'django'", "'django-components'"]], "f1fc17e4a31c71f4d9265f1122da52e7cf57addb4dfa02606e303b33d6431b9b", 1742503665225, 16.51, [0.0035043, 0.0046301], [0.003593, 0.0047163], [0.0035112, 0.0046363], [0.0035924, 0.0047145], [1, 1], [25, 25]], "Components vs Django.timeraw_render_sm_subsequent": [[0.00010400499999718704, 0.0005328339999977061], [["'django'", "'django-components'"]], "6fce1cd85a9344fee383b40a22f27862120b9488a628420625592dc14e0307d3", 1742503668520, 16.499, [0.00010269, 0.00052112], [0.00010566, 0.00054112], [0.00010277, 0.00052114], [0.00010541, 0.00054099], [1, 1], [25, 25]], "Components vs Django.timeraw_startup_lg": [[0.21775109000003567, 0.21398552899995593], [["'django'", "'django-components'"]], "53151821c128ad0ecfb0707fff3146e1abd8d0bcfa301aa056b5d3fae3d793e2", 1742503671830, 19.442, [0.21622, 0.2132], [0.22078, 0.217], [0.21658, 0.21324], [0.22069, 0.2155], [1, 1], [25, 25]], "Other.timeraw_import_time": [[0.19832900800003017], [], "a0a1c1c0db22509410b946d0d4384b52ea4a09b47b6048d7d1cfb89b0c7fe5c3", 1742503675760, 7.9408, [0.19702], [0.20107], [0.19715], [0.20055], [1], [25]], "isolated vs django modes.timeraw_render_lg_first": [[0.2574955810000006, 0.2591010970000127], [["'isolated'", "'django'"]], "f94af83427c6346f88f8785a3cd2fc42415ac5a9fbbdb7de71d27e22e6a81699", 1742503680343, 36.144, [0.25466, 0.25782], [0.25904, 0.26157], [0.25503, 0.25788], [0.25902, 0.2613], [1, 1], [25, 25]], "isolated vs django modes.timeraw_render_lg_subsequent": [[0.14273938200000202, 0.1464969190000147], [["'isolated'", "'django'"]], "9f7c2fde6b33f0451a1794ed903c48d96cd7822f67da502cec36fe8e977c2414", 1742503687671, 45.015, [0.1414, 0.14565], [0.14564, 0.14848], [0.14161, 0.14579], [0.14534, 0.14838], [1, 1], [25, 25]], "isolated vs django modes.timeraw_render_sm_first": [[0.004720848000005162, 0.004705489000002672], [["'isolated'", "'django'"]], "d15ca68909d7f1f43ff16863befb6f42681f17461417fc0069eefd6db3569296", 1742503696639, 16.62, [0.0046697, 0.0046577], [0.00477, 0.0047524], [0.0046797, 0.0046634], [0.0047686, 0.0047508], [1, 1], [25, 25]], "isolated vs django modes.timeraw_render_sm_subsequent": [[0.0005377129999999397, 0.0005395769999836375], [["'isolated'", "'django'"]], "7444bc9516dd087e3f420349345eae991ad6941bbd22fce45265b18034b7cf77", 1742503699956, 16.713, [0.00052335, 0.00052522], [0.00054538, 0.00054226], [0.00052633, 0.00052673], [0.00054385, 0.00054214], [1, 1], [25, 25]], "isolated vs django modes.timeraw_startup_lg": [[0.21402431699999624, 0.21364062999998623], [["'isolated'", "'django'"]], "eabe311ebee4a15c5816617be12f00ec30376f7506bd668219e1c50bc897c134", 1742503703278, 19.101, [0.21272, 0.21292], [0.21716, 0.21483], [0.21299, 0.21294], [0.21691, 0.21473], [1, 1], [25, 25]], "Components vs Django.peakmem_render_lg_first": [[52920320, 54566912], [["'django'", "'django-components'"]], "301c396f017f45a5b3f71e85df58d15f54153fcfd951af7ef424641d4b31b528", 1742503649537, 0.74708], "Components vs Django.peakmem_render_lg_subsequent": [[53800960, 54734848], [["'django'", "'django-components'"]], "9a44e9999ef3ef42ea7e01323727490244febb43d66a87a4d8f88c6b8a133b8b", 1742503650284, 0.93262], "Components vs Django.peakmem_render_sm_first": [[44191744, 44191744], [["'django'", "'django-components'"]], "e93b7a5193681c883edf85bdb30b1bc0821263bf51033fdcee215b155085e036", 1742503651217, 0.35476], "Components vs Django.peakmem_render_sm_subsequent": [[44195840, 44187648], [["'django'", "'django-components'"]], "b46e0820b18950aa7cc5e61306ff3425b76b4da9dca42d64fae5b1d25c6c9026", 1742503651572, 0.35467], "isolated vs django modes.peakmem_render_lg_first": [[54439936, 53968896], [["'isolated'", "'django'"]], "c4bf0016d48d210f08b8db733b57c7dcba1cebbf548c458b93b86ace387067e9", 1742503677373, 0.96761], "isolated vs django modes.peakmem_render_lg_subsequent": [[54968320, 54792192], [["'isolated'", "'django'"]], "65bb1b8586487197a79bb6073e4c71642877b845b6eb42d1bd32398299daffbf", 1742503678341, 1.2663], "isolated vs django modes.peakmem_render_sm_first": [[44187648, 44183552], [["'isolated'", "'django'"]], "c51b91fc583295776062822225e720b5ed71aef9c9288217c401c54283c62840", 1742503679607, 0.36851], "isolated vs django modes.peakmem_render_sm_subsequent": [[44187648, 44187648], [["'isolated'", "'django'"]], "54d747fb8f40179b7ff3d2fc49eb195909ad1c880b5ef7b82f82742b27b67260", 1742503679976, 0.36668]}, "durations": {}, "version": 2} \ No newline at end of file diff --git a/.asv/results/ci-linux/fdd29baa-virtualenv-py3.13-django5.1-djc-core-html-parser.json b/.asv/results/ci-linux/fdd29baa-virtualenv-py3.13-django5.1-djc-core-html-parser.json new file mode 100644 index 00000000..bae22ff4 --- /dev/null +++ b/.asv/results/ci-linux/fdd29baa-virtualenv-py3.13-django5.1-djc-core-html-parser.json @@ -0,0 +1 @@ +{"commit_hash": "fdd29baa65e9ef78eb24a0ad2ca0b5d7c624dad3", "env_name": "virtualenv-py3.13-django5.1-djc-core-html-parser", "date": 1743837055000, "params": {"machine": "ci-linux", "python": "3.13", "django": "5.1", "djc-core-html-parser": ""}, "python": "3.13", "requirements": {"django": "5.1", "djc-core-html-parser": ""}, "env_vars": {}, "result_columns": ["result", "params", "version", "started_at", "duration", "stats_ci_99_a", "stats_ci_99_b", "stats_q_25", "stats_q_75", "stats_number", "stats_repeat", "samples", "profile"], "results": {"Components vs Django.timeraw_render_lg_first": [[0.07297276199997782, 0.2569234329999972], [["'django'", "'django-components'"]], "be3bf6236960046a028b6ea007aad28b2337fc2b906b8ce317a09a5d4f1a6193", 1743837476879, 30.873, [0.071985, 0.25428], [0.074103, 0.26075], [0.072038, 0.25476], [0.07398, 0.26005], [1, 1], [25, 25]], "Components vs Django.timeraw_render_lg_subsequent": [[0.03658580800001232, 0.1459621130000528], [["'django'", "'django-components'"]], "b98221c11a0ee6e9de0778d416d31b9dd514a674d9017a2bb9b2fc1cd0f01920", 1743837482929, 36.367, [0.036404, 0.14414], [0.037604, 0.14778], [0.036407, 0.14431], [0.037516, 0.14769], [1, 1], [25, 25]], "Components vs Django.timeraw_render_sm_first": [[0.0035008030000085455, 0.004695608999981005], [["'django'", "'django-components'"]], "f1fc17e4a31c71f4d9265f1122da52e7cf57addb4dfa02606e303b33d6431b9b", 1743837490117, 16.492, [0.0034613, 0.0046476], [0.0035362, 0.0047717], [0.0034638, 0.0046525], [0.0035327, 0.0047464], [1, 1], [25, 25]], "Components vs Django.timeraw_render_sm_subsequent": [[0.00011641800000461444, 0.0005489540000098714], [["'django'", "'django-components'"]], "6fce1cd85a9344fee383b40a22f27862120b9488a628420625592dc14e0307d3", 1743837493363, 16.416, [0.0001146, 0.00053209], [0.00011763, 0.00055102], [0.00011495, 0.00053218], [0.00011722, 0.00055099], [1, 1], [25, 25]], "Components vs Django.timeraw_startup_lg": [[0.2166100470000174, 0.21420494400001644], [["'django'", "'django-components'"]], "53151821c128ad0ecfb0707fff3146e1abd8d0bcfa301aa056b5d3fae3d793e2", 1743837496617, 19.507, [0.21578, 0.21377], [0.22038, 0.21873], [0.21585, 0.21391], [0.22012, 0.21833], [1, 1], [25, 25]], "Other.timeraw_import_time": [[0.19625152499997967], [], "a0a1c1c0db22509410b946d0d4384b52ea4a09b47b6048d7d1cfb89b0c7fe5c3", 1743837500450, 7.846, [0.19508], [0.1987], [0.19556], [0.19776], [1], [25]], "isolated vs django modes.timeraw_render_lg_first": [[0.2570519909999689, 0.2606809000000112], [["'isolated'", "'django'"]], "f94af83427c6346f88f8785a3cd2fc42415ac5a9fbbdb7de71d27e22e6a81699", 1743837504912, 35.956, [0.25574, 0.25887], [0.26068, 0.26247], [0.25581, 0.25948], [0.26037, 0.26206], [1, 1], [25, 25]], "isolated vs django modes.timeraw_render_lg_subsequent": [[0.14520097999997006, 0.14991973799999414], [["'isolated'", "'django'"]], "9f7c2fde6b33f0451a1794ed903c48d96cd7822f67da502cec36fe8e977c2414", 1743837512048, 45.026, [0.14407, 0.14843], [0.14758, 0.15221], [0.14409, 0.1487], [0.14648, 0.15174], [1, 1], [25, 25]], "isolated vs django modes.timeraw_render_sm_first": [[0.004671787999996013, 0.004672599999992144], [["'isolated'", "'django'"]], "d15ca68909d7f1f43ff16863befb6f42681f17461417fc0069eefd6db3569296", 1743837521015, 16.514, [0.0046336, 0.00463], [0.0047529, 0.0047334], [0.0046393, 0.00464], [0.0047259, 0.0047218], [1, 1], [25, 25]], "isolated vs django modes.timeraw_render_sm_subsequent": [[0.000542692999999872, 0.0005430530000012368], [["'isolated'", "'django'"]], "7444bc9516dd087e3f420349345eae991ad6941bbd22fce45265b18034b7cf77", 1743837524265, 16.535, [0.00053046, 0.00052986], [0.00055115, 0.00055085], [0.00053202, 0.00053211], [0.00055088, 0.00055006], [1, 1], [25, 25]], "isolated vs django modes.timeraw_startup_lg": [[0.2147675530000015, 0.21506381099999317], [["'isolated'", "'django'"]], "eabe311ebee4a15c5816617be12f00ec30376f7506bd668219e1c50bc897c134", 1743837527532, 19.077, [0.21332, 0.21271], [0.21696, 0.21734], [0.21358, 0.21356], [0.21674, 0.21727], [1, 1], [25, 25]], "Components vs Django.peakmem_render_lg_first": [[52379648, 54992896], [["'django'", "'django-components'"]], "301c396f017f45a5b3f71e85df58d15f54153fcfd951af7ef424641d4b31b528", 1743837474462, 0.76167], "Components vs Django.peakmem_render_lg_subsequent": [[51998720, 55451648], [["'django'", "'django-components'"]], "9a44e9999ef3ef42ea7e01323727490244febb43d66a87a4d8f88c6b8a133b8b", 1743837475225, 0.95029], "Components vs Django.peakmem_render_sm_first": [[44195840, 44314624], [["'django'", "'django-components'"]], "e93b7a5193681c883edf85bdb30b1bc0821263bf51033fdcee215b155085e036", 1743837476175, 0.35143], "Components vs Django.peakmem_render_sm_subsequent": [[44322816, 44314624], [["'django'", "'django-components'"]], "b46e0820b18950aa7cc5e61306ff3425b76b4da9dca42d64fae5b1d25c6c9026", 1743837476527, 0.35175], "isolated vs django modes.peakmem_render_lg_first": [[54992896, 54345728], [["'isolated'", "'django'"]], "c4bf0016d48d210f08b8db733b57c7dcba1cebbf548c458b93b86ace387067e9", 1743837501990, 0.94949], "isolated vs django modes.peakmem_render_lg_subsequent": [[55455744, 55177216], [["'isolated'", "'django'"]], "65bb1b8586487197a79bb6073e4c71642877b845b6eb42d1bd32398299daffbf", 1743837502940, 1.2595], "isolated vs django modes.peakmem_render_sm_first": [[44314624, 44314624], [["'isolated'", "'django'"]], "c51b91fc583295776062822225e720b5ed71aef9c9288217c401c54283c62840", 1743837504200, 0.3547], "isolated vs django modes.peakmem_render_sm_subsequent": [[44314624, 44314624], [["'isolated'", "'django'"]], "54d747fb8f40179b7ff3d2fc49eb195909ad1c880b5ef7b82f82742b27b67260", 1743837504555, 0.35682]}, "durations": {}, "version": 2} \ No newline at end of file diff --git a/.asv/results/ci-linux/machine.json b/.asv/results/ci-linux/machine.json new file mode 100644 index 00000000..2e8a88df --- /dev/null +++ b/.asv/results/ci-linux/machine.json @@ -0,0 +1,4 @@ +{ + "machine": "ci-linux", + "version": 1 +} \ No newline at end of file diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 00000000..9558bffd --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,32 @@ +// For format details, see https://aka.ms/devcontainer.json. For config options, see the README at: +// https://github.com/microsoft/vscode-dev-containers/tree/v0.245.0/containers/python-3 +{ + // Uncomment to run Python 3.13 or other specific version + // "image": "mcr.microsoft.com/devcontainers/python:3.13-bullseye", + + // Configure tool-specific properties. + "customizations": { + // Configure properties specific to VS Code. + "vscode": { + // Set *default* container specific settings.json values on container create. + "settings": { + "python.defaultInterpreterPath": "/usr/local/bin/python", + "python.linting.enabled": true + }, + + // Add the IDs of extensions you want installed when the container is created. + "extensions": [ + "ms-python.python", + "ms-python.vscode-pylance", + "ms-python.vscode-python-envs", + "jurooravec.python-inline-source-2" + ] + } + } + + // Use 'forwardPorts' to make a list of ports inside the container available locally. + // "forwardPorts": [], + + // Use 'postCreateCommand' to run commands after the container is created. + //"postCreateCommand": "" +} diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 17c5ae2a..aff066ed 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -9,3 +9,9 @@ updates: directory: "/" # Location of package manifests schedule: interval: "weekly" + - package-ecosystem: github-actions + # This actually targets ./.github/workflows/ + # See https://docs.github.com/en/code-security/dependabot/working-with-dependabot/dependabot-options-reference#directories-or-directory-- + directory: "/" + schedule: + interval: monthly diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index e846e7ce..d13f870d 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -9,11 +9,6 @@ on: - '[0-9]+.[0-9]+.[0-9]+' branches: - master - pull_request: - branches: - - main - release: - types: [published] workflow_dispatch: jobs: @@ -25,25 +20,139 @@ jobs: id-token: write # to verify the deployment originates from an appropriate source runs-on: ubuntu-latest # Only run in original repo (not in forks) - if: github.repository == 'EmilStenstrom/django-components' + if: github.repository == 'django-components/django-components' steps: + + ############################## + # SETUP + ############################## + + # Authenticate with git with the Github App that has permission + # to push to master, in order to push benchmark results. + # See https://stackoverflow.com/a/79142962/9788634 + - uses: actions/create-github-app-token@v2 + id: app-token + with: + app-id: ${{ vars.RELEASE_BOT_APP_ID }} + private-key: ${{ secrets.RELEASE_BOT_APP_PRIVATE_KEY }} + + - name: Checkout + uses: actions/checkout@v4 + with: + token: ${{ steps.app-token.outputs.token }} + fetch-depth: 0 + + - name: Configure git account + run: | + git config user.name components-release-bot + git config user.email "components-release-bot@users.noreply.github.com" + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.13" + cache: 'pip' + + - name: Install dependencies + run: | + python -m pip install --upgrade pip wheel + # NOTE: pin virtualenv to <20.31 until asv fixes it. + # See https://github.com/airspeed-velocity/asv/issues/1484 + python -m pip install -q hatch pre-commit asv virtualenv==20.30 + hatch --version + + ########################################### + # RECORD BENCHMARK - ONLY ON PUSH TO MASTER + ########################################### + + - name: Run benchmarks for tag + if: github.ref_type == 'tag' && github.event_name == 'push' + env: + # See https://github.com/github/docs/issues/21930 + # And https://docs.github.com/en/actions/security-for-github-actions/security-guides/automatic-token-authentication + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + # Get the master branch so we can run benchmarks on it + git remote add upstream https://github.com/${{ github.repository }}.git + git fetch origin master:master + git checkout master + + # Get tag name + TAG=${GITHUB_REF#refs/tags/} + echo "TAG: $TAG" + + # TODO: REMOVE ONCE FIXED UPSTREAM + # Fix for https://github.com/airspeed-velocity/asv_runner/issues/45 + # Prepare virtual environment + # Currently, we have to monkeypatch the `timeit` function in the `timeraw` benchmark. + # The problem is that `asv` passes the code to execute via command line, and when the + # code is too big, it fails with `OSError: [Errno 7] Argument list too long`. + # So we have to tweak it to pass the code via STDIN, which doesn't have this limitation. + # + # 1. First create the virtual environment, so that asv generates the directories where + # the monkeypatch can be applied. + echo "Creating virtual environment..." + asv setup -v || true + echo "Virtual environment created." + # 2. Now let's apply the monkeypatch by appending it to the `timeraw.py` files. + # First find all `timeraw.py` files + echo "Applying monkeypatch..." + find .asv/env -type f -path "*/site-packages/asv_runner/benchmarks/timeraw.py" | while read -r file; do + # Add a newline and then append the monkeypatch contents + echo "" >> "$file" + cat "benchmarks/monkeypatch_asv_ci.txt" >> "$file" + done + echo "Monkeypatch applied." + # END OF MONKEYPATCH + + # Prepare the profile under which the benchmarks will be saved. + # We assume that the CI machine has a name that is unique and stable. + # See https://github.com/airspeed-velocity/asv/issues/796#issuecomment-1188431794 + echo "Preparing benchmarks profile..." + asv machine --yes --machine ci-linux + echo "Benchmarks profile DONE." + + # Run benchmarks for the current tag + # - `^` means that we mean the COMMIT of the tag's branch, not the BRANCH itself. + # Without it, we would run benchmarks for the whole branch history. + # With it, we run benchmarks FROM the tag's commit (incl) TO ... + # - `!` means that we want to select range spanning a single commit. + # Without it, we would run benchmarks for all commits FROM the tag's commit + # TO the start of the branch history. + # With it, we run benchmarks ONLY FOR the tag's commit. + echo "Running benchmarks for tag ${TAG}..." + asv run master^! -v + echo "Benchmarks for tag ${TAG} DONE." + + # Generate benchmarks site + # This should save it in `docs/benchmarks/`, so we can then use it when + # building docs site with `mkdocs`. + echo "Generating benchmarks site..." + asv publish + echo "Benchmarks site DONE." + + # Commit benchmark results + echo "Staging and committing benchmark results..." + git add .asv/results/ + git add docs/benchmarks/ + git commit -m "Add benchmark results for ${TAG}" + echo "Benchmark results committed." + + # Push to the new branch + echo "Pushing benchmark results..." + git push origin master + echo "Benchmark results pushed to master." + + ########################################### + # BUILD & RELEASE DOCS + ########################################### + + # Change git authentication to Github Actions, so the rest of the + # workflow will have lower privileges. - name: Checkout uses: actions/checkout@v4 with: fetch-depth: 0 - - name: Set up Python - uses: actions/setup-python@v5 - with: - python-version: "3.12" - - - name: Install Hatch - run: | - python -m pip install --upgrade pip wheel - python -m pip install -q hatch pre-commit - hatch --version - - - name: Create Virtual Environment - run: hatch env create docs - name: Configure git run: | @@ -64,17 +173,23 @@ jobs: - name: Build & deploy "dev" docs for a new commit to master if: github.event_name == 'push' && github.ref_type != 'tag' run: | + # Fetch and checkout gh-pages to ensure we have the latest version + git fetch origin gh-pages + git checkout gh-pages + git pull origin gh-pages + git checkout master + export SHORT_SHA=$(echo "${GITHUB_SHA}" | cut -c1-7) hatch run docs:mike deploy --push --update-aliases --title "dev (${SHORT_SHA})" dev - name: Build & deploy docs for a new tag if: github.ref_type == 'tag' && github.event_name == 'push' run: | - hatch run docs:mike deploy --push --update-aliases ${{ github.ref_name }} latest - hatch run docs:mike set-default latest --push + # Fetch and checkout gh-pages to ensure we have the latest version + git fetch origin gh-pages + git checkout gh-pages + git pull origin gh-pages + git checkout master - - name: Build & deploy docs for a new release - if: github.event_name == 'release' - run: | hatch run docs:mike deploy --push --update-aliases ${{ github.ref_name }} latest hatch run docs:mike set-default latest --push diff --git a/.github/workflows/pr-benchmark-comment.yml b/.github/workflows/pr-benchmark-comment.yml new file mode 100644 index 00000000..b80a13bd --- /dev/null +++ b/.github/workflows/pr-benchmark-comment.yml @@ -0,0 +1,99 @@ +# Run benchmark report on pull requests to master. +# The report is added to the PR as a comment. +# +# NOTE: When making a PR from a fork, the worker doesn't have sufficient +# access to make comments on the target repo's PR. And so, this workflow +# is split to two parts: +# +# 1. Benchmarking and saving results as artifacts +# 2. Downloading the results and commenting on the PR +# +# See https://stackoverflow.com/a/71683208/9788634 + +name: PR benchmark comment + +on: + workflow_run: + # NOTE: The name here MUST match the name of the workflow that generates the data + workflows: [PR benchmarks generate] + types: + - completed + +jobs: + download: + runs-on: ubuntu-latest + permissions: + contents: write + pull-requests: write + repository-projects: write + steps: + ########## USE FOR DEBUGGING ########## + - name: Debug workflow run info + uses: actions/github-script@v7 + with: + script: | + console.log('Workflow Run ID:', context.payload.workflow_run.id); + const artifacts = await github.rest.actions.listWorkflowRunArtifacts({ + owner: context.repo.owner, + repo: context.repo.repo, + run_id: context.payload.workflow_run.id + }); + console.log('Available artifacts:'); + console.log(JSON.stringify(artifacts.data, null, 2)); + console.log(`PRs: ` + JSON.stringify(context.payload.workflow_run.pull_requests)); + ######################################### + + # NOTE: The next two steps (download and unzip) are equivalent to using `actions/download-artifact@v4` + # However, `download-artifact` was not picking up the artifact, while the REST client does. + - name: Download benchmark results + uses: actions/github-script@v7 + with: + script: | + const fs = require('fs'); + + // Find the artifact that was generated by the "pr-benchmark-generate" workflow + const allArtifacts = await github.rest.actions.listWorkflowRunArtifacts({ + owner: context.repo.owner, + repo: context.repo.repo, + // Explicitly search the workflow run that generated the the results + // (AKA the "pr-benchmark-generate" workflow). + run_id: context.payload.workflow_run.id, + }); + const matchArtifact = allArtifacts.data.artifacts.filter((artifact) => { + return artifact.name == "benchmark_results" + })[0]; + + // Download the artifact + const download = await github.rest.actions.downloadArtifact({ + owner: context.repo.owner, + repo: context.repo.repo, + artifact_id: matchArtifact.id, + archive_format: 'zip', + }); + fs.writeFileSync( + `${process.env.GITHUB_WORKSPACE}/benchmark_results.zip`, + Buffer.from(download.data), + ); + + - name: Unzip artifact + run: unzip benchmark_results.zip + + - name: Comment on PR + # See https://github.com/actions/github-script + uses: actions/github-script@v7 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const fs = require('fs'); + const results = fs.readFileSync('./benchmark_results.md', 'utf8'); + const issue_number = Number.parseInt(fs.readFileSync('./pr_number.txt', 'utf8')); + const body = `## Performance Benchmark Results\n\nComparing PR changes against master branch:\n\n${results}`; + + // See https://octokit.github.io/rest.js/v21/#issues-create-comment + await github.rest.issues.createComment({ + body: body, + // See https://github.com/actions/toolkit/blob/662b9d91f584bf29efbc41b86723e0e376010e41/packages/github/src/context.ts#L66 + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: issue_number, + }); diff --git a/.github/workflows/pr-benchmark-generate.yml b/.github/workflows/pr-benchmark-generate.yml new file mode 100644 index 00000000..ff92ad5f --- /dev/null +++ b/.github/workflows/pr-benchmark-generate.yml @@ -0,0 +1,112 @@ +# Run benchmark report on pull requests to master. +# The report is added to the PR as a comment. +# +# NOTE: When making a PR from a fork, the worker doesn't have sufficient +# access to make comments on the target repo's PR. And so, this workflow +# is split to two parts: +# +# 1. Benchmarking and saving results as artifacts +# 2. Downloading the results and commenting on the PR +# +# See https://stackoverflow.com/a/71683208/9788634 + +name: PR benchmarks generate + +on: + pull_request: + branches: [ master ] + +jobs: + benchmark: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 # Need full history for ASV + + - name: Fetch base branch + run: | + git remote add upstream https://github.com/${{ github.repository }}.git + git fetch upstream master + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.13' + cache: 'pip' + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + # NOTE: pin virtualenv to <20.31 until asv fixes it. + # See https://github.com/airspeed-velocity/asv/issues/1484 + pip install asv virtualenv==20.30 + + - name: Run benchmarks + run: | + # TODO: REMOVE ONCE FIXED UPSTREAM + # Fix for https://github.com/airspeed-velocity/asv_runner/issues/45 + # Prepare virtual environment + # Currently, we have to monkeypatch the `timeit` function in the `timeraw` benchmark. + # The problem is that `asv` passes the code to execute via command line, and when the + # code is too big, it fails with `OSError: [Errno 7] Argument list too long`. + # So we have to tweak it to pass the code via STDIN, which doesn't have this limitation. + # + # 1. First create the virtual environment, so that asv generates the directories where + # the monkeypatch can be applied. + echo "Creating virtual environment..." + asv setup -v || true + echo "Virtual environment created." + # 2. Now let's apply the monkeypatch by appending it to the `timeraw.py` files. + # First find all `timeraw.py` files + echo "Applying monkeypatch..." + find .asv/env -type f -path "*/site-packages/asv_runner/benchmarks/timeraw.py" | while read -r file; do + # Add a newline and then append the monkeypatch contents + echo "" >> "$file" + cat "benchmarks/monkeypatch_asv_ci.txt" >> "$file" + done + echo "Monkeypatch applied." + # END OF MONKEYPATCH + + # Prepare the profile under which the benchmarks will be saved. + # We assume that the CI machine has a name that is unique and stable. + # See https://github.com/airspeed-velocity/asv/issues/796#issuecomment-1188431794 + echo "Preparing benchmarks profile..." + MACHINE="ci_benchmark_${{ github.event.pull_request.number }}" + asv machine --yes -v --machine ${MACHINE} + echo "Benchmarks profile DONE." + + # Generate benchmark data + # - `^` means that we mean the COMMIT of the branch, not the BRANCH itself. + # Without it, we would run benchmarks for the whole branch history. + # With it, we run benchmarks FROM the latest commit (incl) TO ... + # - `!` means that we want to select range spanning a single commit. + # Without it, we would run benchmarks for all commits FROM the latest commit + # TO the start of the branch history. + # With it, we run benchmarks ONLY FOR the latest commit. + echo "Running benchmarks for upstream/master..." + DJC_BENCHMARK_QUICK=1 asv run upstream/master^! -v --machine ${MACHINE} + echo "Benchmarks for upstream/master DONE." + echo "Running benchmarks for HEAD..." + DJC_BENCHMARK_QUICK=1 asv run HEAD^! -v --machine ${MACHINE} + echo "Benchmarks for HEAD DONE." + + echo "Creating pr directory..." + mkdir -p pr + # Save the PR number to a file, so that it can be used by the next step. + echo "${{ github.event.pull_request.number }}" > ./pr/pr_number.txt + + # Compare against master + # NOTE: The command is run twice, once so we can see the debug output, and once to save the results. + echo "Comparing benchmarks... (debug)" + asv compare upstream/master HEAD --factor 1.1 --split --machine ${MACHINE} --verbose + echo "Comparing benchmarks... (saving results)" + asv compare upstream/master HEAD --factor 1.1 --split --machine ${MACHINE} > ./pr/benchmark_results.md + echo "Benchmarks comparison DONE." + + - name: Save benchmark results + uses: actions/upload-artifact@v4 + with: + name: benchmark_results + path: pr/ diff --git a/.github/workflows/publish-to-pypi.yml b/.github/workflows/publish-to-pypi.yml index f89b2535..f3e0b170 100644 --- a/.github/workflows/publish-to-pypi.yml +++ b/.github/workflows/publish-to-pypi.yml @@ -12,15 +12,15 @@ on: jobs: build: runs-on: ubuntu-latest - if: github.repository == 'EmilStenstrom/django-components' + if: github.repository == 'django-components/django-components' steps: - name: Checkout the repo - uses: actions/checkout@v2 + uses: actions/checkout@v4 - name: Setup python - uses: actions/setup-python@v2 + uses: actions/setup-python@v5 with: - python-version: '3.9' + python-version: '3.13' - name: Install pypa/build run: >- diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index f1506e21..bc0ecb99 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -14,9 +14,19 @@ jobs: strategy: matrix: python-version: ['3.8', '3.9', '3.10', '3.11', '3.12', '3.13'] - os: [ubuntu-20.04, windows-latest] + os: [ubuntu-latest, windows-latest] steps: + # Configure git to handle long paths + # See https://stackoverflow.com/questions/22575662/filename-too-long-in-git-for-windows + # + # Long paths that are over the limit are because of the benchmarking data + # created by asv, as these may look like this: + # docs/benchmarks/graphs/arch-x86_64/branch-master/cpu-AMD EPYC 7763 64-Core Processor/django-5.1/djc-core-html-parser/machine-fv-az1693-854/num_cpu-4/os-Linux 6.8.0-1021-azure/python-3.13/ram-16373792/isolated vs django modes.timeraw_render_lg_subsequent.json + - name: Configure git + run: | + git config --global core.longpaths true + - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} @@ -35,7 +45,7 @@ jobs: # Verify that docs build test_docs: - runs-on: ubuntu-20.04 + runs-on: ubuntu-latest strategy: matrix: python-version: ['3.13'] @@ -57,3 +67,35 @@ jobs: python -m pip install -e . - name: Build documentation run: mkdocs build --verbose + + # Verify that the sample project works + test_sampleproject: + runs-on: ubuntu-latest + strategy: + matrix: + python-version: ['3.13'] + + steps: + - uses: actions/checkout@v4 + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + cache: "pip" + + - name: Install dependencies + run: | + cd sampleproject + python -m pip install --upgrade pip + python -m pip install -r requirements.txt + # Install django-components locally + python -m pip install -e .. + - name: Check Django project + run: | + cd sampleproject + python manage.py check + python manage.py migrate --noinput + # Start the server, make request, and exit with error if it fails + python manage.py runserver & sleep 5 + curl http://127.0.0.1:8000/ || exit 1 diff --git a/.gitignore b/.gitignore index 50cb9c64..3d7aeebe 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,6 @@ +# Project-specific files +sampleproject/staticfiles/ + # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] @@ -43,6 +46,7 @@ htmlcov/ nosetests.xml coverage.xml *,cover +.pytest_cache/ # Translations *.mo @@ -76,7 +80,11 @@ poetry.lock site .direnv/ .envrc +.mypy_cache/ # JS, NPM Dependency directories node_modules/ jspm_packages/ + +# Cursor +.cursorrules \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 4f4610d2..9cabbd37 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,22 +1,1927 @@ # Release notes +## v0.141.2 + +#### Fix + +- Fix bug where JS and CSS were missing when `{% component %}` tag was inside `{% include %}` tag ([#1296](https://github.com/django-components/django-components/issues/1296)) + +## v0.141.1 + +#### Fix + +- Components' JS and CSS scripts (e.g. from `Component.js` or `Component.js_file`) are now cached at class creation time. + + This means that when you now restart the server while having a page opened in the browser, + the JS / CSS files are immediately available. + + Previously, the JS/CSS were cached only after the components were rendered. So you had to reload + the page to trigger the rendering, in order to make the JS/CSS files available. + +- Fix the default cache for JS / CSS scripts to be unbounded. + + Previously, the default cache for the JS/CSS scripts (`LocMemCache`) was accidentally limited to 300 entries (~150 components). + +- Do not send `template_rendered` signal when rendering a component with no template. ([#1277](https://github.com/django-components/django-components/issues/1277)) + +## v0.141.0 + +#### Feat + +- New extension hooks `on_template_loaded`, `on_js_loaded`, `on_css_loaded`, and `on_template_compiled` + + The first 3 hooks are called when Component's template / JS / CSS is loaded as a string. + + The `on_template_compiled` hook is called when Component's template is compiled to a Template. + + The `on_xx_loaded` hooks can modify the content by returning the new value. + + ```py + class MyExtension(ComponentExtension): + def on_template_loaded(self, ctx: OnTemplateLoadedContext) -> Optional[str]: + return ctx.content + "" + + def on_js_loaded(self, ctx: OnJsLoadedContext) -> Optional[str]: + return ctx.content + "// Hello!" + + def on_css_loaded(self, ctx: OnCssLoadedContext) -> Optional[str]: + return ctx.content + "/* Hello! */" + ``` + + See all [Extension hooks](https://django-components.github.io/django-components/0.141.0/reference/extension_hooks/). + +#### Fix + +- Subclassing - Previously, if a parent component defined `Component.template` or `Component.template_file`, it's subclass would use the same `Template` instance. + + This could lead to unexpected behavior, where a change to the template of the subclass would also change the template of the parent class. + + Now, each subclass has it's own `Template` instance, and changes to the template of the subclass do not affect the template of the parent class. + +- Fix Django failing to restart due to "TypeError: 'Dynamic' object is not iterable" ([#1232](https://github.com/django-components/django-components/issues/1232)) + +- Fix bug when error formatting failed when error value was not a string. + +#### Refactor + +- `components ext run` CLI command now allows to call only those extensions that actually have subcommands. + +## v0.140.1 + +#### Fix + +- Fix typo preventing benchmarking ([#1235](https://github.com/django-components/django-components/pull/1235)) + +## 🚨📢 v0.140.0 + +⚠️ Major release ⚠️ - Please test thoroughly before / after upgrading. + +This is the biggest step towards v1. While this version introduces +many small API changes, we don't expect to make further changes to +the affected parts before v1. + +For more details see [#433](https://github.com/django-components/django-components/issues/433). + +Summary: + +- Overhauled typing system +- Middleware removed, no longer needed +- `get_template_data()` is the new canonical way to define template data. + `get_context_data()` is now deprecated but will remain until v2. +- Slots API polished and prepared for v1. +- Merged `Component.Url` with `Component.View` +- Added `Component.args`, `Component.kwargs`, `Component.slots`, `Component.context` +- Added `{{ component_vars.args }}`, `{{ component_vars.kwargs }}`, `{{ component_vars.slots }}` +- You should no longer instantiate `Component` instances. Instead, call `Component.render()` or `Component.render_to_response()` directly. +- Component caching can now consider slots (opt-in) +- And lot more... + +#### 🚨📢 BREAKING CHANGES + +**Middleware** + +- The middleware `ComponentDependencyMiddleware` was removed as it is no longer needed. + + The middleware served one purpose - to render the JS and CSS dependencies of components + when you rendered templates with `Template.render()` or `django.shortcuts.render()` and those templates contained `{% component %}` tags. + + - NOTE: If you rendered HTML with `Component.render()` or `Component.render_to_response()`, the JS and CSS were already rendered. + + Now, the JS and CSS dependencies of components are automatically rendered, + even when you render Templates with `Template.render()` or `django.shortcuts.render()`. + + To disable this behavior, set the `DJC_DEPS_STRATEGY` context key to `"ignore"` + when rendering the template: + + ```py + # With `Template.render()`: + template = Template(template_str) + rendered = template.render(Context({"DJC_DEPS_STRATEGY": "ignore"})) + + # Or with django.shortcuts.render(): + from django.shortcuts import render + rendered = render( + request, + "my_template.html", + context={"DJC_DEPS_STRATEGY": "ignore"}, + ) + ``` + + In fact, you can set the `DJC_DEPS_STRATEGY` context key to any of the strategies: + + - `"document"` + - `"fragment"` + - `"simple"` + - `"prepend"` + - `"append"` + - `"ignore"` + + See [Dependencies rendering](https://django-components.github.io/django-components/0.140.1/concepts/advanced/rendering_js_css/) for more info. + +**Typing** + +- Component typing no longer uses generics. Instead, the types are now defined as class attributes of the component class. + + Before: + + ```py + Args = Tuple[float, str] + + class Button(Component[Args]): + pass + ``` + + After: + + ```py + class Button(Component): + class Args(NamedTuple): + size: float + text: str + ``` + + + See [Migrating from generics to class attributes](https://django-components.github.io/django-components/0.140.1/concepts/fundamentals/typing_and_validation/#migrating-from-generics-to-class-attributes) for more info. +- Removed `EmptyTuple` and `EmptyDict` types. Instead, there is now a single `Empty` type. + + ```py + from django_components import Component, Empty + + class Button(Component): + template = "Hello" + + Args = Empty + Kwargs = Empty + ``` + +**Component API** + +- The interface of the not-yet-released `get_js_data()` and `get_css_data()` methods has changed to + match `get_template_data()`. + + Before: + + ```py + def get_js_data(self, *args, **kwargs): + def get_css_data(self, *args, **kwargs): + ``` + + After: + + ```py + def get_js_data(self, args, kwargs, slots, context): + def get_css_data(self, args, kwargs, slots, context): + ``` + +- Arguments in `Component.render_to_response()` have changed + to match that of `Component.render()`. + + Please ensure that you pass the parameters as kwargs, not as positional arguments, + to avoid breaking changes. + + The signature changed, moving the `args` and `kwargs` parameters to 2nd and 3rd position. + + Next, the `render_dependencies` parameter was added to match `Component.render()`. + + Lastly: + + - Previously, any extra ARGS and KWARGS were passed to the `response_class`. + - Now, only extra KWARGS will be passed to the `response_class`. + + Before: + + ```py + def render_to_response( + cls, + context: Optional[Union[Dict[str, Any], Context]] = None, + slots: Optional[SlotsType] = None, + escape_slots_content: bool = True, + args: Optional[ArgsType] = None, + kwargs: Optional[KwargsType] = None, + deps_strategy: DependenciesStrategy = "document", + request: Optional[HttpRequest] = None, + *response_args: Any, + **response_kwargs: Any, + ) -> HttpResponse: + ``` + + After: + + ```py + def render_to_response( + context: Optional[Union[Dict[str, Any], Context]] = None, + args: Optional[Any] = None, + kwargs: Optional[Any] = None, + slots: Optional[Any] = None, + deps_strategy: DependenciesStrategy = "document", + type: Optional[DependenciesStrategy] = None, # Deprecated, use `deps_strategy` + render_dependencies: bool = True, # Deprecated, use `deps_strategy="ignore"` + outer_context: Optional[Context] = None, + request: Optional[HttpRequest] = None, + registry: Optional[ComponentRegistry] = None, + registered_name: Optional[str] = None, + node: Optional[ComponentNode] = None, + **response_kwargs: Any, + ) -> HttpResponse: + ``` + +- `Component.render()` and `Component.render_to_response()` NO LONGER accept `escape_slots_content` kwarg. + + Instead, slots are now always escaped. + + To disable escaping, wrap the result of `slots` in + [`mark_safe()`](https://docs.djangoproject.com/en/5.2/ref/utils/#django.utils.safestring.mark_safe). + + Before: + + ```py + html = component.render( + slots={"my_slot": "CONTENT"}, + escape_slots_content=False, + ) + ``` + + After: + + ```py + html = component.render( + slots={"my_slot": mark_safe("CONTENT")} + ) + ``` + +- `Component.template` no longer accepts a Template instance, only plain string. + + Before: + + ```py + class MyComponent(Component): + template = Template("{{ my_var }}") + ``` + + Instead, either: + + 1. Set `Component.template` to a plain string. + + ```py + class MyComponent(Component): + template = "{{ my_var }}" + ``` + + 2. Move the template to it's own HTML file and set `Component.template_file`. + + ```py + class MyComponent(Component): + template_file = "my_template.html" + ``` + + 3. Or, if you dynamically created the template, render the template inside `Component.on_render()`. + + ```py + class MyComponent(Component): + def on_render(self, context, template): + dynamic_template = do_something_dynamic() + return dynamic_template.render(context) + ``` + +- Subclassing of components with `None` values has changed: + + Previously, when a child component's template / JS / CSS attributes were set to `None`, the child component still inherited the parent's template / JS / CSS. + + Now, the child component will not inherit the parent's template / JS / CSS if it sets the attribute to `None`. + + Before: + + ```py + class Parent(Component): + template = "parent.html" + + class Child(Parent): + template = None + + # Child still inherited parent's template + assert Child.template == Parent.template + ``` + + After: + + ```py + class Parent(Component): + template = "parent.html" + + class Child(Parent): + template = None + + # Child does not inherit parent's template + assert Child.template is None + ``` + +- The `Component.Url` class was merged with `Component.View`. + + Instead of `Component.Url.public`, use `Component.View.public`. + + If you imported `ComponentUrl` from `django_components`, you need to update your import to `ComponentView`. + + Before: + + ```py + class MyComponent(Component): + class Url: + public = True + + class View: + def get(self, request): + return self.render_to_response() + ``` + + After: + + ```py + class MyComponent(Component): + class View: + public = True + + def get(self, request): + return self.render_to_response() + ``` + +- Caching - The function signatures of `Component.Cache.get_cache_key()` and `Component.Cache.hash()` have changed to enable passing slots. + + Args and kwargs are no longer spread, but passed as a list and a dict, respectively. + + Before: + + ```py + def get_cache_key(self, *args: Any, **kwargs: Any) -> str: + + def hash(self, *args: Any, **kwargs: Any) -> str: + ``` + + After: + + ```py + def get_cache_key(self, args: Any, kwargs: Any, slots: Any) -> str: + + def hash(self, args: Any, kwargs: Any) -> str: + ``` + +**Template tags** + +- Component name in the `{% component %}` tag can no longer be set as a kwarg. + + Instead, the component name MUST be the first POSITIONAL argument only. + + Before, it was possible to set the component name as a kwarg + and put it anywhere in the `{% component %}` tag: + + ```django + {% component rows=rows headers=headers name="my_table" ... / %} + ``` + + Now, the component name MUST be the first POSITIONAL argument: + + ```django + {% component "my_table" rows=rows headers=headers ... / %} + ``` + + Thus, the `name` kwarg can now be used as a regular input. + + ```django + {% component "profile" name="John" job="Developer" / %} + ``` + +**Slots** + +- If you instantiated `Slot` class with kwargs, you should now use `contents` instead of `content_func`. + + Before: + + ```py + slot = Slot(content_func=lambda *a, **kw: "CONTENT") + ``` + + After: + + ```py + slot = Slot(contents=lambda ctx: "CONTENT") + ``` + + Alternatively, pass the function / content as first positional argument: + + ```py + slot = Slot(lambda ctx: "CONTENT") + ``` + +- The undocumented `Slot.escaped` attribute was removed. + + Instead, slots are now always escaped. + + To disable escaping, wrap the result of `slots` in + [`mark_safe()`](https://docs.djangoproject.com/en/5.2/ref/utils/#django.utils.safestring.mark_safe). + +- Slot functions behavior has changed. See the new [Slots](https://django-components.github.io/django-components/latest/concepts/fundamentals/slots/) docs for more info. + + - Function signature: + + 1. All parameters are now passed under a single `ctx` argument. + + You can still access all the same parameters via `ctx.context`, `ctx.data`, and `ctx.fallback`. + + 2. `context` and `fallback` now may be `None` if the slot function was called outside of `{% slot %}` tag. + + Before: + + ```py + def slot_fn(context: Context, data: Dict, slot_ref: SlotRef): + isinstance(context, Context) + isinstance(data, Dict) + isinstance(slot_ref, SlotRef) + + return "CONTENT" + ``` + + After: + + ```py + def slot_fn(ctx: SlotContext): + assert isinstance(ctx.context, Context) # May be None + assert isinstance(ctx.data, Dict) + assert isinstance(ctx.fallback, SlotFallback) # May be None + + return "CONTENT" + ``` + + - Calling slot functions: + + 1. Rather than calling the slot functions directly, you should now call the `Slot` instances. + + 2. All parameters are now optional. + + 3. The order of parameters has changed. + + Before: + + ```py + def slot_fn(context: Context, data: Dict, slot_ref: SlotRef): + return "CONTENT" + + html = slot_fn(context, data, slot_ref) + ``` + + After: + + ```py + def slot_fn(ctx: SlotContext): + return "CONTENT" + + slot = Slot(slot_fn) + html = slot() + html = slot({"data1": "abc", "data2": "hello"}) + html = slot({"data1": "abc", "data2": "hello"}, fallback="FALLBACK") + ``` + + - Usage in components: + + Before: + + ```python + class MyComponent(Component): + def get_context_data(self, *args, **kwargs): + slots = self.input.slots + slot_fn = slots["my_slot"] + html = slot_fn(context, data, slot_ref) + return { + "html": html, + } + ``` + + After: + + ```python + class MyComponent(Component): + def get_template_data(self, args, kwargs, slots, context): + slot_fn = slots["my_slot"] + html = slot_fn(data) + return { + "html": html, + } + ``` + +**Miscellaneous** + +- The second argument to `render_dependencies()` is now `strategy` instead of `type`. + + Before: + + ```py + render_dependencies(content, type="document") + ``` + + After: + + ```py + render_dependencies(content, strategy="document") + ``` + +#### 🚨📢 Deprecation + +**Component API** + +- `Component.get_context_data()` is now deprecated. Use `Component.get_template_data()` instead. + + `get_template_data()` behaves the same way, but has a different function signature + to accept also slots and context. + + Since `get_context_data()` is widely used, it will remain available until v2. + +- `Component.get_template_name()` and `Component.get_template()` are now deprecated. Use `Component.template`, +`Component.template_file` or `Component.on_render()` instead. + + `Component.get_template_name()` and `Component.get_template()` will be removed in v1. + + In v1, each Component will have at most one static template. + This is needed to enable support for Markdown, Pug, or other pre-processing of templates by extensions. + + If you are using the deprecated methods to point to different templates, there's 2 ways to migrate: + + 1. Split the single Component into multiple Components, each with its own template. Then switch between them in `Component.on_render()`: + + ```py + class MyComponentA(Component): + template_file = "a.html" + + class MyComponentB(Component): + template_file = "b.html" + + class MyComponent(Component): + def on_render(self, context, template): + if context["a"]: + return MyComponentA.render(context) + else: + return MyComponentB.render(context) + ``` + + 2. Alternatively, use `Component.on_render()` with Django's `get_template()` to dynamically render different templates: + + ```py + from django.template.loader import get_template + + class MyComponent(Component): + def on_render(self, context, template): + if context["a"]: + template_name = "a.html" + else: + template_name = "b.html" + + actual_template = get_template(template_name) + return actual_template.render(context) + ``` + + Read more in [django-components#1204](https://github.com/django-components/django-components/discussions/1204). + +- The `type` kwarg in `Component.render()` and `Component.render_to_response()` is now deprecated. Use `deps_strategy` instead. The `type` kwarg will be removed in v1. + + Before: + + ```py + Calendar.render_to_response(type="fragment") + ``` + + After: + + ```py + Calendar.render_to_response(deps_strategy="fragment") + ``` + +- The `render_dependencies` kwarg in `Component.render()` and `Component.render_to_response()` is now deprecated. Use `deps_strategy="ignore"` instead. The `render_dependencies` kwarg will be removed in v1. + + Before: + + ```py + Calendar.render_to_response(render_dependencies=False) + ``` + + After: + + ```py + Calendar.render_to_response(deps_strategy="ignore") + ``` + +- Support for `Component` constructor kwargs `registered_name`, `outer_context`, and `registry` is deprecated, and will be removed in v1. + + Before, you could instantiate a standalone component, + and then call `render()` on the instance: + + ```py + comp = MyComponent( + registered_name="my_component", + outer_context=my_context, + registry=my_registry, + ) + comp.render( + args=[1, 2, 3], + kwargs={"a": 1, "b": 2}, + slots={"my_slot": "CONTENT"}, + ) + ``` + + Now you should instead pass all that data to `Component.render()` / `Component.render_to_response()`: + + ```py + MyComponent.render( + args=[1, 2, 3], + kwargs={"a": 1, "b": 2}, + slots={"my_slot": "CONTENT"}, + # NEW + registered_name="my_component", + outer_context=my_context, + registry=my_registry, + ) + ``` + +- `Component.input` (and its type `ComponentInput`) is now deprecated. The `input` property will be removed in v1. + + Instead, use attributes directly on the Component instance. + + Before: + + ```py + class MyComponent(Component): + def on_render(self, context, template): + assert self.input.args == [1, 2, 3] + assert self.input.kwargs == {"a": 1, "b": 2} + assert self.input.slots == {"my_slot": "CONTENT"} + assert self.input.context == {"my_slot": "CONTENT"} + assert self.input.deps_strategy == "document" + assert self.input.type == "document" + assert self.input.render_dependencies == True + ``` + + After: + + ```py + class MyComponent(Component): + def on_render(self, context, template): + assert self.args == [1, 2, 3] + assert self.kwargs == {"a": 1, "b": 2} + assert self.slots == {"my_slot": "CONTENT"} + assert self.context == {"my_slot": "CONTENT"} + assert self.deps_strategy == "document" + assert (self.deps_strategy != "ignore") is True + ``` + +- Component method `on_render_after` was updated to receive also `error` field. + + For backwards compatibility, the `error` field can be omitted until v1. + + Before: + + ```py + def on_render_after( + self, + context: Context, + template: Template, + html: str, + ) -> None: + pass + ``` + + After: + + ```py + def on_render_after( + self, + context: Context, + template: Template, + html: Optional[str], + error: Optional[Exception], + ) -> None: + pass + ``` + +- If you are using the Components as views, the way to access the component class is now different. + + Instead of `self.component`, use `self.component_cls`. `self.component` will be removed in v1. + + Before: + + ```py + class MyView(View): + def get(self, request): + return self.component.render_to_response(request=request) + ``` + + After: + + ```py + class MyView(View): + def get(self, request): + return self.component_cls.render_to_response(request=request) + ``` + +**Extensions** + +- In the `on_component_data()` extension hook, the `context_data` field of the context object was superseded by `template_data`. + + The `context_data` field will be removed in v1.0. + + Before: + + ```py + class MyExtension(ComponentExtension): + def on_component_data(self, ctx: OnComponentDataContext) -> None: + ctx.context_data["my_template_var"] = "my_value" + ``` + + After: + + ```py + class MyExtension(ComponentExtension): + def on_component_data(self, ctx: OnComponentDataContext) -> None: + ctx.template_data["my_template_var"] = "my_value" + ``` + +- When creating extensions, the `ComponentExtension.ExtensionClass` attribute was renamed to `ComponentConfig`. + + The old name is deprecated and will be removed in v1. + + Before: + + ```py + from django_components import ComponentExtension + + class MyExtension(ComponentExtension): + class ExtensionClass(ComponentExtension.ExtensionClass): + pass + ``` + + After: + + ```py + from django_components import ComponentExtension, ExtensionComponentConfig + + class MyExtension(ComponentExtension): + class ComponentConfig(ExtensionComponentConfig): + pass + ``` + +- When creating extensions, to access the Component class from within the methods of the extension nested classes, + use `component_cls`. + + Previously this field was named `component_class`. The old name is deprecated and will be removed in v1. + + `ComponentExtension.ExtensionClass` attribute was renamed to `ComponentConfig`. + + The old name is deprecated and will be removed in v1. + + Before: + + ```py + from django_components import ComponentExtension, ExtensionComponentConfig + + class LoggerExtension(ComponentExtension): + name = "logger" + + class ComponentConfig(ExtensionComponentConfig): + def log(self, msg: str) -> None: + print(f"{self.component_class.__name__}: {msg}") + ``` + + After: + + ```py + from django_components import ComponentExtension, ExtensionComponentConfig + + class LoggerExtension(ComponentExtension): + name = "logger" + + class ComponentConfig(ExtensionComponentConfig): + def log(self, msg: str) -> None: + print(f"{self.component_cls.__name__}: {msg}") + ``` + +**Slots** + +- `SlotContent` was renamed to `SlotInput`. The old name is deprecated and will be removed in v1. + +- `SlotRef` was renamed to `SlotFallback`. The old name is deprecated and will be removed in v1. + +- The `default` kwarg in `{% fill %}` tag was renamed to `fallback`. The old name is deprecated and will be removed in v1. + + Before: + + ```django + {% fill "footer" default="footer" %} + {{ footer }} + {% endfill %} + ``` + + After: + + ```django + {% fill "footer" fallback="footer" %} + {{ footer }} + {% endfill %} + ``` + +- The template variable `{{ component_vars.is_filled }}` is now deprecated. Will be removed in v1. Use `{{ component_vars.slots }}` instead. + + Before: + + ```django + {% if component_vars.is_filled.footer %} +
+ {% slot "footer" / %} +
+ {% endif %} + ``` + + After: + + ```django + {% if component_vars.slots.footer %} +
+ {% slot "footer" / %} +
+ {% endif %} + ``` + + NOTE: `component_vars.is_filled` automatically escaped slot names, so that even slot names that are + not valid python identifiers could be set as slot names. `component_vars.slots` no longer does that. + +- Component attribute `Component.is_filled` is now deprecated. Will be removed in v1. Use `Component.slots` instead. + + Before: + + ```py + class MyComponent(Component): + def get_template_data(self, args, kwargs, slots, context): + if self.is_filled.footer: + color = "red" + else: + color = "blue" + + return { + "color": color, + } + ``` + + After: + + ```py + class MyComponent(Component): + def get_template_data(self, args, kwargs, slots, context): + if "footer" in slots: + color = "red" + else: + color = "blue" + + return { + "color": color, + } + ``` + + NOTE: `Component.is_filled` automatically escaped slot names, so that even slot names that are + not valid python identifiers could be set as slot names. `Component.slots` no longer does that. + +**Miscellaneous** + +- Template caching with `cached_template()` helper and `template_cache_size` setting is deprecated. + These will be removed in v1. + + This feature made sense if you were dynamically generating templates for components using + `Component.get_template_string()` and `Component.get_template()`. + + However, in v1, each Component will have at most one static template. This static template + is cached internally per component class, and reused across renders. + + This makes the template caching feature obsolete. + + If you relied on `cached_template()`, you should either: + + 1. Wrap the templates as Components. + 2. Manage the cache of Templates yourself. + +- The `debug_highlight_components` and `debug_highlight_slots` settings are deprecated. + These will be removed in v1. + + The debug highlighting feature was re-implemented as an extension. + As such, the recommended way for enabling it has changed: + + Before: + + ```python + COMPONENTS = ComponentsSettings( + debug_highlight_components=True, + debug_highlight_slots=True, + ) + ``` + + After: + + Set `extensions_defaults` in your `settings.py` file. + + ```python + COMPONENTS = ComponentsSettings( + extensions_defaults={ + "debug_highlight": { + "highlight_components": True, + "highlight_slots": True, + }, + }, + ) + ``` + + Alternatively, you can enable highlighting for specific components by setting `Component.DebugHighlight.highlight_components` to `True`: + + ```python + class MyComponent(Component): + class DebugHighlight: + highlight_components = True + highlight_slots = True + ``` + +#### Feat + +- New method to render template variables - `get_template_data()` + + `get_template_data()` behaves the same way as `get_context_data()`, but has + a different function signature to accept also slots and context. + + ```py + class Button(Component): + def get_template_data(self, args, kwargs, slots, context): + return { + "val1": args[0], + "val2": kwargs["field"], + } + ``` + + If you define `Component.Args`, `Component.Kwargs`, `Component.Slots`, then + the `args`, `kwargs`, `slots` arguments will be instances of these classes: + + ```py + class Button(Component): + class Args(NamedTuple): + field1: str + + class Kwargs(NamedTuple): + field2: int + + def get_template_data(self, args: Args, kwargs: Kwargs, slots, context): + return { + "val1": args.field1, + "val2": kwargs.field2, + } + ``` + +- Input validation is now part of the render process. + + When you specify the input types (such as `Component.Args`, `Component.Kwargs`, etc), + the actual inputs to data methods (`Component.get_template_data()`, etc) will be instances of the types you specified. + + This practically brings back input validation, because the instantiation of the types + will raise an error if the inputs are not valid. + + Read more on [Typing and validation](https://django-components.github.io/django-components/latest/concepts/fundamentals/typing_and_validation/) + +- Render emails or other non-browser HTML with new "dependencies strategies" + + When rendering a component with `Component.render()` or `Component.render_to_response()`, + the `deps_strategy` kwarg (previously `type`) now accepts additional options: + + - `"simple"` + - `"prepend"` + - `"append"` + - `"ignore"` + + ```py + Calendar.render_to_response( + request=request, + kwargs={ + "date": request.GET.get("date", ""), + }, + deps_strategy="append", + ) + ``` + + Comparison of dependencies render strategies: + + - `"document"` + - Smartly inserts JS / CSS into placeholders or into `` and `` tags. + - Inserts extra script to allow `fragment` strategy to work. + - Assumes the HTML will be rendered in a JS-enabled browser. + - `"fragment"` + - A lightweight HTML fragment to be inserted into a document with AJAX. + - Ignores placeholders and any `` / `` tags. + - No JS / CSS included. + - `"simple"` + - Smartly insert JS / CSS into placeholders or into `` and `` tags. + - No extra script loaded. + - `"prepend"` + - Insert JS / CSS before the rendered HTML. + - Ignores placeholders and any `` / `` tags. + - No extra script loaded. + - `"append"` + - Insert JS / CSS after the rendered HTML. + - Ignores placeholders and any `` / `` tags. + - No extra script loaded. + - `"ignore"` + - Rendered HTML is left as-is. You can still process it with a different strategy later with `render_dependencies()`. + - Used for inserting rendered HTML into other components. + + See [Dependencies rendering](https://django-components.github.io/django-components/0.140.1/concepts/advanced/rendering_js_css/) for more info. + +- New `Component.args`, `Component.kwargs`, `Component.slots` attributes available on the component class itself. + + These attributes are the same as the ones available in `Component.get_template_data()`. + + You can use these in other methods like `Component.on_render_before()` or `Component.on_render_after()`. + + ```py + from django_components import Component, SlotInput + + class Table(Component): + class Args(NamedTuple): + page: int + + class Kwargs(NamedTuple): + per_page: int + + class Slots(NamedTuple): + content: SlotInput + + def on_render_before(self, context: Context, template: Optional[Template]) -> None: + assert self.args.page == 123 + assert self.kwargs.per_page == 10 + content_html = self.slots.content() + ``` + + Same as with the parameters in `Component.get_template_data()`, they will be instances of the `Args`, `Kwargs`, `Slots` classes + if defined, or plain lists / dictionaries otherwise. + +- 4 attributes that were previously available only under the `Component.input` attribute + are now available directly on the Component instance: + + - `Component.raw_args` + - `Component.raw_kwargs` + - `Component.raw_slots` + - `Component.deps_strategy` + + The first 3 attributes are the same as the deprecated `Component.input.args`, `Component.input.kwargs`, `Component.input.slots` properties. + + Compared to the `Component.args` / `Component.kwargs` / `Component.slots` attributes, + these "raw" attributes are not typed and will remain as plain lists / dictionaries + even if you define the `Args`, `Kwargs`, `Slots` classes. + + The `Component.deps_strategy` attribute is the same as the deprecated `Component.input.deps_strategy` property. + +- New template variables `{{ component_vars.args }}`, `{{ component_vars.kwargs }}`, `{{ component_vars.slots }}` + + These attributes are the same as the ones available in `Component.get_template_data()`. + + ```django + {# Typed #} + {% if component_vars.args.page == 123 %} +
+ {% slot "content" / %} +
+ {% endif %} + + {# Untyped #} + {% if component_vars.args.0 == 123 %} +
+ {% slot "content" / %} +
+ {% endif %} + ``` + + Same as with the parameters in `Component.get_template_data()`, they will be instances of the `Args`, `Kwargs`, `Slots` classes + if defined, or plain lists / dictionaries otherwise. + +- New component lifecycle hook `Component.on_render()`. + + This hook is called when the component is being rendered. + + You can override this method to: + + - Change what template gets rendered + - Modify the context + - Modify the rendered output after it has been rendered + - Handle errors + + See [on_render](https://django-components.github.io/django-components/0.140.1/concepts/advanced/hooks/#on_render) for more info. + +- `get_component_url()` now optionally accepts `query` and `fragment` arguments. + + ```py + from django_components import get_component_url + + url = get_component_url( + MyComponent, + query={"foo": "bar"}, + fragment="baz", + ) + # /components/ext/view/components/c1ab2c3?foo=bar#baz + ``` + +- The `BaseNode` class has a new `contents` attribute, which contains the raw contents (string) of the tag body. + + This is relevant when you define custom template tags with `@template_tag` decorator or `BaseNode` class. + + When you define a custom template tag like so: + + ```py + from django_components import BaseNode, template_tag + + @template_tag( + library, + tag="mytag", + end_tag="endmytag", + allowed_flags=["required"] + ) + def mytag(node: BaseNode, context: Context, name: str, **kwargs) -> str: + print(node.contents) + return f"Hello, {name}!" + ``` + + And render it like so: + + ```django + {% mytag name="John" %} + Hello, world! + {% endmytag %} + ``` + + Then, the `contents` attribute of the `BaseNode` instance will contain the string `"Hello, world!"`. + +- The `BaseNode` class also has two new metadata attributes: + + - `template_name` - the name of the template that rendered the node. + - `template_component` - the component class that the template belongs to. + + This is useful for debugging purposes. + +- `Slot` class now has 3 new metadata fields: + + 1. `Slot.contents` attribute contains the original contents: + + - If `Slot` was created from `{% fill %}` tag, `Slot.contents` will contain the body of the `{% fill %}` tag. + - If `Slot` was created from string via `Slot("...")`, `Slot.contents` will contain that string. + - If `Slot` was created from a function, `Slot.contents` will contain that function. + + 2. `Slot.extra` attribute where you can put arbitrary metadata about the slot. + + 3. `Slot.fill_node` attribute tells where the slot comes from: + + - `FillNode` instance if the slot was created from `{% fill %}` tag. + - `ComponentNode` instance if the slot was created as a default slot from a `{% component %}` tag. + - `None` if the slot was created from a string, function, or `Slot` instance. + + See [Slot metadata](https://django-components.github.io/django-components/0.140.1/concepts/fundamentals/slots/#slot-metadata). + +- `{% fill %}` tag now accepts `body` kwarg to pass a Slot instance to fill. + + First pass a `Slot` instance to the template + with the `get_template_data()` method: + + ```python + from django_components import component, Slot + + class Table(Component): + def get_template_data(self, args, kwargs, slots, context): + return { + "my_slot": Slot(lambda ctx: "Hello, world!"), + } + ``` + + Then pass the slot to the `{% fill %}` tag: + + ```django + {% component "table" %} + {% fill "pagination" body=my_slot / %} + {% endcomponent %} + ``` + +- You can now access the `{% component %}` tag (`ComponentNode` instance) from which a Component + was created. Use `Component.node` to access it. + + This is mostly useful for extensions, which can use this to detect if the given Component + comes from a `{% component %}` tag or from a different source (such as `Component.render()`). + + `Component.node` is `None` if the component is created by `Component.render()` (but you + can pass in the `node` kwarg yourself). + + ```py + class MyComponent(Component): + def get_template_data(self, context, template): + if self.node is not None: + assert self.node.name == "my_component" + ``` + +- Node classes `ComponentNode`, `FillNode`, `ProvideNode`, and `SlotNode` are part of the public API. + + These classes are what is instantiated when you use `{% component %}`, `{% fill %}`, `{% provide %}`, and `{% slot %}` tags. + + You can for example use these for type hints: + + ```py + from django_components import Component, ComponentNode + + class MyTable(Component): + def get_template_data(self, args, kwargs, slots, context): + if kwargs.get("show_owner"): + node: Optional[ComponentNode] = self.node + owner: Optional[Component] = self.node.template_component + else: + node = None + owner = None + + return { + "owner": owner, + "node": node, + } + ``` + +- Component caching can now take slots into account, by setting `Component.Cache.include_slots` to `True`. + + ```py + class MyComponent(Component): + class Cache: + enabled = True + include_slots = True + ``` + + In which case the following two calls will generate separate cache entries: + + ```django + {% component "my_component" position="left" %} + Hello, Alice + {% endcomponent %} + + {% component "my_component" position="left" %} + Hello, Bob + {% endcomponent %} + ``` + + Same applies to `Component.render()` with string slots: + + ```py + MyComponent.render( + kwargs={"position": "left"}, + slots={"content": "Hello, Alice"} + ) + MyComponent.render( + kwargs={"position": "left"}, + slots={"content": "Hello, Bob"} + ) + ``` + + Read more on [Component caching](https://django-components.github.io/django-components/0.140.1/concepts/advanced/component_caching/). + +- New extension hook `on_slot_rendered()` + + This hook is called when a slot is rendered, and allows you to access and/or modify the rendered result. + + This is used by the ["debug highlight" feature](https://django-components.github.io/django-components/0.140.1/guides/other/troubleshooting/#component-and-slot-highlighting). + + To modify the rendered result, return the new value: + + ```py + class MyExtension(ComponentExtension): + def on_slot_rendered(self, ctx: OnSlotRenderedContext) -> Optional[str]: + return ctx.result + "" + ``` + + If you don't want to modify the rendered result, return `None`. + + See all [Extension hooks](https://django-components.github.io/django-components/0.140.1/reference/extension_hooks/). + +- When creating extensions, the previous syntax with `ComponentExtension.ExtensionClass` was causing + Mypy errors, because Mypy doesn't allow using class attributes as bases: + + Before: + + ```py + from django_components import ComponentExtension + + class MyExtension(ComponentExtension): + class ExtensionClass(ComponentExtension.ExtensionClass): # Error! + pass + ``` + + Instead, you can import `ExtensionComponentConfig` directly: + + After: + + ```py + from django_components import ComponentExtension, ExtensionComponentConfig + + class MyExtension(ComponentExtension): + class ComponentConfig(ExtensionComponentConfig): + pass + ``` + +#### Refactor + +- When a component is being rendered, a proper `Component` instance is now created. + + Previously, the `Component` state was managed as half-instance, half-stack. + +- Component's "Render API" (args, kwargs, slots, context, inputs, request, context data, etc) + can now be accessed also outside of the render call. So now its possible to take the component + instance out of `get_template_data()` (although this is not recommended). + +- Components can now be defined without a template. + + Previously, the following would raise an error: + + ```py + class MyComponent(Component): + pass + ``` + + "Template-less" components can be used together with `Component.on_render()` to dynamically + pick what to render: + + ```py + class TableNew(Component): + template_file = "table_new.html" + + class TableOld(Component): + template_file = "table_old.html" + + class Table(Component): + def on_render(self, context, template): + if self.kwargs.get("feat_table_new_ui"): + return TableNew.render(args=self.args, kwargs=self.kwargs, slots=self.slots) + else: + return TableOld.render(args=self.args, kwargs=self.kwargs, slots=self.slots) + ``` + + "Template-less" components can be also used as a base class for other components, or as mixins. + +- Passing `Slot` instance to `Slot` constructor raises an error. + +- Extension hook `on_component_rendered` now receives `error` field. + + `on_component_rendered` now behaves similar to `Component.on_render_after`: + + - Raising error in this hook overrides what error will be returned from `Component.render()`. + - Returning new string overrides what will be returned from `Component.render()`. + + Before: + + ```py + class OnComponentRenderedContext(NamedTuple): + component: "Component" + component_cls: Type["Component"] + component_id: str + result: str + ``` + + After: + + ```py + class OnComponentRenderedContext(NamedTuple): + component: "Component" + component_cls: Type["Component"] + component_id: str + result: Optional[str] + error: Optional[Exception] + ``` + +#### Fix + +- Fix bug: Context processors data was being generated anew for each component. Now the data is correctly created once and reused across components with the same request ([#1165](https://github.com/django-components/django-components/issues/1165)). + +- Fix KeyError on `component_context_cache` when slots are rendered outside of the component's render context. ([#1189](https://github.com/django-components/django-components/issues/1189)) + +- Component classes now have `do_not_call_in_templates=True` to prevent them from being called as functions in templates. + +## v0.139.1 + +#### Fix + +- Fix compatibility of component caching with `{% extend %}` block ([#1135](https://github.com/django-components/django-components/issues/1135)) + +#### Refactor + +- Component ID is now prefixed with `c`, e.g. `c123456`. + +- When typing a Component, you can now specify as few or as many parameters as you want. + + ```py + Component[Args] + Component[Args, Kwargs] + Component[Args, Kwargs, Slots] + Component[Args, Kwargs, Slots, Data] + Component[Args, Kwargs, Slots, Data, JsData] + Component[Args, Kwargs, Slots, Data, JsData, CssData] + ``` + + All omitted parameters will default to `Any`. + +- Added `typing_extensions` to the project as a dependency + +- Multiple extensions with the same name (case-insensitive) now raise an error + +- Extension names (case-insensitive) also MUST NOT conflict with existing Component class API. + + So if you name an extension `render`, it will conflict with the `render()` method of the `Component` class, + and thus raise an error. + +## v0.139.0 + +#### Fix + +- Fix bug: Fix compatibility with `Finder.find()` in Django 5.2 ([#1119](https://github.com/django-components/django-components/issues/1119)) + +## v0.138 + +#### Fix + +- Fix bug: Allow components with `Url.public = True` to be defined before `django.setup()` + +## v0.137 + +#### Feat + +- Each Component class now has a `class_id` attribute, which is unique to the component subclass. + + NOTE: This is different from `Component.id`, which is unique to each rendered instance. + + To look up a component class by its `class_id`, use `get_component_by_class_id()`. + +- It's now easier to create URLs for component views. + + Before, you had to call `Component.as_view()` and pass that to `urlpatterns`. + + Now this can be done for you if you set `Component.Url.public` to `True`: + + ```py + class MyComponent(Component): + class Url: + public = True + ... + ``` + + Then, to get the URL for the component, use `get_component_url()`: + + ```py + from django_components import get_component_url + + url = get_component_url(MyComponent) + ``` + + This way you don't have to mix your app URLs with component URLs. + + Read more on [Component views and URLs](https://django-components.github.io/django-components/0.137/concepts/fundamentals/component_views_urls/). + +- Per-component caching - Set `Component.Cache.enabled` to `True` to enable caching for a component. + + Component caching allows you to store the rendered output of a component. Next time the component is rendered + with the same input, the cached output is returned instead of re-rendering the component. + + ```py + class TestComponent(Component): + template = "Hello" + + class Cache: + enabled = True + ttl = 0.1 # .1 seconds TTL + cache_name = "custom_cache" + + # Custom hash method for args and kwargs + # NOTE: The default implementation simply serializes the input into a string. + # As such, it might not be suitable for complex objects like Models. + def hash(self, *args, **kwargs): + return f"{json.dumps(args)}:{json.dumps(kwargs)}" + + ``` + + Read more on [Component caching](https://django-components.github.io/django-components/0.137/concepts/advanced/component_caching/). + +- `@djc_test` can now be called without first calling `django.setup()`, in which case it does it for you. + +- Expose `ComponentInput` class, which is a typing for `Component.input`. + +#### Deprecation + +- Currently, view request handlers such as `get()` and `post()` methods can be defined + directly on the `Component` class: + + ```py + class MyComponent(Component): + def get(self, request): + return self.render_to_response() + ``` + + Or, nested within the `Component.View` class: + + ```py + class MyComponent(Component): + class View: + def get(self, request): + return self.render_to_response() + ``` + + In v1, these methods should be defined only on the `Component.View` class instead. + +#### Refactor + +- `Component.get_context_data()` can now omit a return statement or return `None`. + +## 🚨📢 v0.136 + +#### 🚨📢 BREAKING CHANGES + +- Component input validation was moved to a separate extension [`djc-ext-pydantic`](https://github.com/django-components/djc-ext-pydantic). + + If you relied on components raising errors when inputs were invalid, you need to install `djc-ext-pydantic` and add it to extensions: + + ```python + # settings.py + COMPONENTS = { + "extensions": [ + "djc_pydantic.PydanticExtension", + ], + } + ``` + +#### Fix + +- Make it possible to resolve URLs added by extensions by their names + +## v0.135 + +#### Feat + +- Add defaults for the component inputs with the `Component.Defaults` nested class. Defaults + are applied if the argument is not given, or if it set to `None`. + + For lists, dictionaries, or other objects, wrap the value in `Default()` class to mark it as a factory + function: + + ```python + from django_components import Default + + class Table(Component): + class Defaults: + position = "left" + width = "200px" + options = Default(lambda: ["left", "right", "center"]) + + def get_context_data(self, position, width, options): + return { + "position": position, + "width": width, + "options": options, + } + + # `position` is used as given, `"right"` + # `width` uses default because it's `None` + # `options` uses default because it's missing + Table.render( + kwargs={ + "position": "right", + "width": None, + } + ) + ``` + +- `{% html_attrs %}` now offers a Vue-like granular control over `class` and `style` HTML attributes, +where each class name or style property can be managed separately. + + ```django + {% html_attrs + class="foo bar" + class={"baz": True, "foo": False} + class="extra" + %} + ``` + + ```django + {% html_attrs + style="text-align: center; background-color: blue;" + style={"background-color": "green", "color": None, "width": False} + style="position: absolute; height: 12px;" + %} + ``` + + Read more on [HTML attributes](https://django-components.github.io/django-components/0.135/concepts/fundamentals/html_attributes/). + +#### Fix + +- Fix compat with Windows when reading component files ([#1074](https://github.com/django-components/django-components/issues/1074)) +- Fix resolution of component media files edge case ([#1073](https://github.com/django-components/django-components/issues/1073)) + +## v0.134 + +#### Fix + +- HOTFIX: Fix the use of URLs in `Component.Media.js` and `Component.Media.css` + +## v0.133 + +⚠️ Attention ⚠️ - Please update to v0.134 to fix bugs introduced in v0.132. + +#### Fix + +- HOTFIX: Fix the use of URLs in `Component.Media.js` and `Component.Media.css` + +## v0.132 + +⚠️ Attention ⚠️ - Please update to v0.134 to fix bugs introduced in v0.132. + +#### Feat + +- Allow to use glob patterns as paths for additional JS / CSS in + `Component.Media.js` and `Component.Media.css` + + ```py + class MyComponent(Component): + class Media: + js = ["*.js"] + css = ["*.css"] + ``` + +#### Fix + +- Fix installation for Python 3.13 on Windows. + +## v0.131 + +#### Feat + +- Support for extensions (plugins) for django-components! + + - Hook into lifecycle events of django-components + - Pre-/post-process component inputs, outputs, and templates + - Add extra methods or attributes to Components + - Add custom extension-specific CLI commands + - Add custom extension-specific URL routes + + Read more on [Extensions](https://django-components.github.io/django-components/0.131/concepts/advanced/extensions/). + +- New CLI commands: + - `components list` - List all components + - `components create ` - Create a new component (supersedes `startcomponent`) + - `components upgrade` - Upgrade a component (supersedes `upgradecomponent`) + - `components ext list` - List all extensions + - `components ext run ` - Run a command added by an extension + +- `@djc_test` decorator for writing tests that involve Components. + + - The decorator manages global state, ensuring that tests don't leak. + - If using `pytest`, the decorator allows you to parametrize Django or Components settings. + - The decorator also serves as a stand-in for Django's `@override_settings`. + + See the API reference for [`@djc_test`](https://django-components.github.io/django-components/0.131/reference/testing_api/#django_components.testing.djc_test) for more details. + +- `ComponentRegistry` now has a `has()` method to check if a component is registered + without raising an error. + +- Get all created `Component` classes with `all_components()`. + +- Get all created `ComponentRegistry` instances with `all_registries()`. + +#### Refactor + +- The `startcomponent` and `upgradecomponent` commands are deprecated, and will be removed in v1. + + Instead, use `components create ` and `components upgrade`. + +#### Internal + +- Settings are now loaded only once, and thus are considered immutable once loaded. Previously, + django-components would load settings from `settings.COMPONENTS` on each access. The new behavior + aligns with Django's settings. + +## v0.130 + +#### Feat + +- Access the HttpRequest object under `Component.request`. + + To pass the request object to a component, either: + - Render a template or component with `RequestContext`, + - Or set the `request` kwarg to `Component.render()` or `Component.render_to_response()`. + + Read more on [HttpRequest](https://django-components.github.io/django-components/0.130/concepts/fundamentals/http_request/). + +- Access the context processors data under `Component.context_processors_data`. + + Context processors data is available only when the component has access to the `request` object, + either by: + - Passing the request to `Component.render()` or `Component.render_to_response()`, + - Or by rendering a template or component with `RequestContext`, + - Or being nested in another component that has access to the request object. + + The data from context processors is automatically available within the component's template. + + Read more on [HttpRequest](https://django-components.github.io/django-components/0.130/concepts/fundamentals/http_request/). + +## v0.129 + +#### Fix + +- Fix thread unsafe media resolve validation by moving it to ComponentMedia `__post_init` ([#977](https://github.com/django-components/django-components/pull/977) +- Fix bug: Relative path in extends and include does not work when using template_file ([#976](https://github.com/django-components/django-components/pull/976) +- Fix error when template cache setting (`template_cache_size`) is set to 0 ([#974](https://github.com/django-components/django-components/pull/974) + +## v0.128 + +#### Feat + +- Configurable cache - Set [`COMPONENTS.cache`](https://django-components.github.io/django-components/0.128/reference/settings/#django_components.app_settings.ComponentsSettings.cache) to change where and how django-components caches JS and CSS files. ([#946](https://github.com/django-components/django-components/pull/946)) + + Read more on [Caching](https://django-components.github.io/django-components/0.128/guides/setup/caching). + +- Highlight coponents and slots in the UI - We've added two boolean settings [`COMPONENTS.debug_highlight_components`](https://django-components.github.io/django-components/0.128/reference/settings/#django_components.app_settings.ComponentsSettings.debug_highlight_components) and [`COMPONENTS.debug_highlight_slots`](https://django-components.github.io/django-components/0.128/reference/settings/#django_components.app_settings.ComponentsSettings.debug_highlight_slots), which can be independently set to `True`. First will wrap components in a blue border, the second will wrap slots in a red border. ([#942](https://github.com/django-components/django-components/pull/942)) + + Read more on [Troubleshooting](https://django-components.github.io/django-components/0.128/guides/other/troubleshooting/#component-and-slot-highlighting). + +#### Refactor + +- Removed use of eval for node validation ([#944](https://github.com/django-components/django-components/pull/944)) + +#### Perf + +- Components can now be infinitely nested. ([#936](https://github.com/django-components/django-components/pull/936)) + +- Component input validation is now 6-7x faster on CPython and PyPy. This previously made up 10-30% of the total render time. ([#945](https://github.com/django-components/django-components/pull/945)) + +## v0.127 + +#### Fix + +- Fix component rendering when using `{% cache %}` with remote cache and multiple web servers ([#930](https://github.com/django-components/django-components/issues/930)) + +## v0.126 + +#### Refactor + +- Replaced [BeautifulSoup4](https://www.crummy.com/software/BeautifulSoup/bs4/doc/) with a custom HTML parser. +- The heuristic for inserting JS and CSS dependenies into the default place has changed. + - JS is still inserted at the end of the ``, and CSS at the end of ``. + - However, we find end of `` by searching for **last** occurrence of `` + - And for the end of `` we search for the **first** occurrence of `` + +## v0.125 + +⚠️ Attention ⚠️ - We migrated from `EmilStenstrom/django-components` to `django-components/django-components`. + +**Repo name and documentation URL changed. Package name remains the same.** + +If you see any broken links or other issues, please report them in [#922](https://github.com/django-components/django-components/issues/922). + +#### Feat + +- `@template_tag` and `BaseNode` - A decorator and a class that allow you to define + custom template tags that will behave similarly to django-components' own template tags. + + Read more on [Template tags](https://django-components.github.io/django-components/0.125/concepts/advanced/template_tags/). + + Template tags defined with `@template_tag` and `BaseNode` will have the following features: + + - Accepting args, kwargs, and flags. + + - Allowing literal lists and dicts as inputs as: + + `key=[1, 2, 3]` or `key={"a": 1, "b": 2}` + - Using template tags tag inputs as: + + `{% my_tag key="{% lorem 3 w %}" / %}` + - Supporting the flat dictionary definition: + + `attr:key=value` + - Spreading args and kwargs with `...`: + + `{% my_tag ...args ...kwargs / %}` + - Being able to call the template tag as: + + `{% my_tag %} ... {% endmy_tag %}` or `{% my_tag / %}` + + +#### Refactor + +- Refactored template tag input validation. When you now call template tags like + `{% slot %}`, `{% fill %}`, `{% html_attrs %}`, and others, their inputs are now + validated the same way as Python function inputs are. + + So, for example + + ```django + {% slot "my_slot" name="content" / %} + ``` + + will raise an error, because the positional argument `name` is given twice. + + NOTE: Special kwargs whose keys are not valid Python variable names are not affected by this change. + So when you define: + + ```django + {% component data-id=123 / %} + ``` + + The `data-id` will still be accepted as a valid kwarg, assuming that your `get_context_data()` + accepts `**kwargs`: + + ```py + def get_context_data(self, **kwargs): + return { + "data_id": kwargs["data-id"], + } + ``` + +## v0.124 + +#### Feat + +- Instead of inlining the JS and CSS under `Component.js` and `Component.css`, you can move + them to their own files, and link the JS/CSS files with `Component.js_file` and `Component.css_file`. + + Even when you specify the JS/CSS with `Component.js_file` or `Component.css_file`, then you can still + access the content under `Component.js` or `Component.css` - behind the scenes, the content of the JS/CSS files + will be set to `Component.js` / `Component.css` upon first access. + + The same applies to `Component.template_file`, which will populate `Component.template` upon first access. + + With this change, the role of `Component.js/css` and the JS/CSS in `Component.Media` has changed: + + - The JS/CSS defined in `Component.js/css` or `Component.js/css_file` is the "main" JS/CSS + - The JS/CSS defined in `Component.Media.js/css` are secondary or additional + + See the updated ["Getting Started" tutorial](https://django-components.github.io/django-components/0.124/getting_started/adding_js_and_css/) + +#### Refactor + +- The canonical way to define a template file was changed from `template_name` to `template_file`, to align with the rest of the API. + + `template_name` remains for backwards compatibility. When you get / set `template_name`, + internally this is proxied to `template_file`. + +- The undocumented `Component.component_id` was removed. Instead, use `Component.id`. Changes: + + - While `component_id` was unique every time you instantiated `Component`, the new `id` is unique + every time you render the component (e.g. with `Component.render()`) + - The new `id` is available only during render, so e.g. from within `get_context_data()` + +- Component's HTML / CSS / JS are now resolved and loaded lazily. That is, if you specify `template_name`/`template_file`, + `js_file`, `css_file`, or `Media.js/css`, the file paths will be resolved only once you: + + 1. Try to access component's HTML / CSS / JS, or + 2. Render the component. + + Read more on [Accessing component's HTML / JS / CSS](https://django-components.github.io/django-components/0.124/concepts/fundamentals/defining_js_css_html_files/#customize-how-paths-are-rendered-into-html-tags). + +- Component inheritance: + + - When you subclass a component, the JS and CSS defined on parent's `Media` class is now inherited by the child component. + - You can disable or customize Media inheritance by setting `extend` attribute on the `Component.Media` nested class. This work similarly to Django's [`Media.extend`](https://docs.djangoproject.com/en/5.2/topics/forms/media/#extend). + - When child component defines either `template` or `template_file`, both of parent's `template` and `template_file` are ignored. The same applies to `js_file` and `css_file`. + +- Autodiscovery now ignores files and directories that start with an underscore (`_`), except `__init__.py` + +- The [Signals](https://docs.djangoproject.com/en/5.2/topics/signals/) emitted by or during the use of django-components are now documented, together the `template_rendered` signal. + ## v0.123 #### Fix -- Fix edge cases around rendering components whose templates used the `{% extends %}` template tag ([#859](https://github.com/EmilStenstrom/django-components/pull/859)) +- Fix edge cases around rendering components whose templates used the `{% extends %}` template tag ([#859](https://github.com/django-components/django-components/pull/859)) ## v0.122 #### Feat -- Add support for HTML fragments. HTML fragments can be rendered by passing `type="fragment"` to `Component.render()` or `Component.render_to_response()`. Read more on how to [use HTML fragments with HTMX, AlpineJS, or vanillaJS](https://EmilStenstrom.github.io/django-components/latest/concepts/advanced/html_tragments). +- Add support for HTML fragments. HTML fragments can be rendered by passing `type="fragment"` to `Component.render()` or `Component.render_to_response()`. Read more on how to [use HTML fragments with HTMX, AlpineJS, or vanillaJS](https://django-components.github.io/django-components/latest/concepts/advanced/html_fragments). ## v0.121 #### Fix -- Fix the use of Django template filters (`|lower:"etc"`) with component inputs [#855](https://github.com/EmilStenstrom/django-components/pull/855). +- Fix the use of Django template filters (`|lower:"etc"`) with component inputs [#855](https://github.com/django-components/django-components/pull/855). ## v0.120 @@ -24,23 +1929,23 @@ #### Fix -- Fix the use of translation strings `_("bla")` as inputs to components [#849](https://github.com/EmilStenstrom/django-components/pull/849). +- Fix the use of translation strings `_("bla")` as inputs to components [#849](https://github.com/django-components/django-components/pull/849). ## v0.119 -⚠️ Attention ⚠️ - This release introduced bugs [#849](https://github.com/EmilStenstrom/django-components/pull/849), [#855](https://github.com/EmilStenstrom/django-components/pull/855). Please update to v0.121. +⚠️ Attention ⚠️ - This release introduced bugs [#849](https://github.com/django-components/django-components/pull/849), [#855](https://github.com/django-components/django-components/pull/855). Please update to v0.121. #### Fix - Fix compatibility with custom subclasses of Django's `Template` that need to access - `origin` or other initialization arguments. (https://github.com/EmilStenstrom/django-components/pull/828) + `origin` or other initialization arguments. (https://github.com/django-components/django-components/pull/828) #### Refactor - Compatibility with `django-debug-toolbar-template-profiler`: - - Monkeypatching of Django's `Template` now happens at `AppConfig.ready()` (https://github.com/EmilStenstrom/django-components/pull/825) + - Monkeypatching of Django's `Template` now happens at `AppConfig.ready()` (https://github.com/django-components/django-components/pull/825) -- Internal parsing of template tags tag was updated. No API change. (https://github.com/EmilStenstrom/django-components/pull/827) +- Internal parsing of template tags tag was updated. No API change. (https://github.com/django-components/django-components/pull/827) ## v0.118 @@ -75,7 +1980,7 @@ ## v0.116 -⚠️ Attention ⚠️ - Please update to v0.117 to fix known bugs. See [#791](https://github.com/EmilStenstrom/django-components/issues/791) and [#789](https://github.com/EmilStenstrom/django-components/issues/789) and [#818](https://github.com/EmilStenstrom/django-components/issues/818). +⚠️ Attention ⚠️ - Please update to v0.117 to fix known bugs. See [#791](https://github.com/django-components/django-components/issues/791) and [#789](https://github.com/django-components/django-components/issues/789) and [#818](https://github.com/django-components/django-components/issues/818). #### Fix @@ -122,7 +2027,7 @@ ## v0.115 -⚠️ Attention ⚠️ - Please update to v0.117 to fix known bugs. See [#791](https://github.com/EmilStenstrom/django-components/issues/791) and [#789](https://github.com/EmilStenstrom/django-components/issues/789) and [#818](https://github.com/EmilStenstrom/django-components/issues/818). +⚠️ Attention ⚠️ - Please update to v0.117 to fix known bugs. See [#791](https://github.com/django-components/django-components/issues/791) and [#789](https://github.com/django-components/django-components/issues/789) and [#818](https://github.com/django-components/django-components/issues/818). #### Fix @@ -131,7 +2036,7 @@ ## v0.114 -⚠️ Attention ⚠️ - Please update to v0.117 to fix known bugs. See [#791](https://github.com/EmilStenstrom/django-components/issues/791) and [#789](https://github.com/EmilStenstrom/django-components/issues/789) and [#818](https://github.com/EmilStenstrom/django-components/issues/818). +⚠️ Attention ⚠️ - Please update to v0.117 to fix known bugs. See [#791](https://github.com/django-components/django-components/issues/791) and [#789](https://github.com/django-components/django-components/issues/789) and [#818](https://github.com/django-components/django-components/issues/818). #### Fix @@ -140,7 +2045,7 @@ ## v0.113 -⚠️ Attention ⚠️ - Please update to v0.117 to fix known bugs. See [#791](https://github.com/EmilStenstrom/django-components/issues/791) and [#789](https://github.com/EmilStenstrom/django-components/issues/789) and [#818](https://github.com/EmilStenstrom/django-components/issues/818). +⚠️ Attention ⚠️ - Please update to v0.117 to fix known bugs. See [#791](https://github.com/django-components/django-components/issues/791) and [#789](https://github.com/django-components/django-components/issues/789) and [#818](https://github.com/django-components/django-components/issues/818). #### Fix @@ -148,7 +2053,7 @@ ## v0.112 -⚠️ Attention ⚠️ - Please update to v0.117 to fix known bugs. See [#791](https://github.com/EmilStenstrom/django-components/issues/791) and [#789](https://github.com/EmilStenstrom/django-components/issues/789) and [#818](https://github.com/EmilStenstrom/django-components/issues/818). +⚠️ Attention ⚠️ - Please update to v0.117 to fix known bugs. See [#791](https://github.com/django-components/django-components/issues/791) and [#789](https://github.com/django-components/django-components/issues/789) and [#818](https://github.com/django-components/django-components/issues/818). #### Fix @@ -156,7 +2061,7 @@ ## v0.111 -⚠️ Attention ⚠️ - Please update to v0.117 to fix known bugs. See [#791](https://github.com/EmilStenstrom/django-components/issues/791) and [#789](https://github.com/EmilStenstrom/django-components/issues/789) and [#818](https://github.com/EmilStenstrom/django-components/issues/818). +⚠️ Attention ⚠️ - Please update to v0.117 to fix known bugs. See [#791](https://github.com/django-components/django-components/issues/791) and [#789](https://github.com/django-components/django-components/issues/789) and [#818](https://github.com/django-components/django-components/issues/818). #### Fix @@ -165,7 +2070,7 @@ ## 🚨📢 v0.110 -⚠️ Attention ⚠️ - Please update to v0.117 to fix known bugs. See [#791](https://github.com/EmilStenstrom/django-components/issues/791) and [#789](https://github.com/EmilStenstrom/django-components/issues/789) and [#818](https://github.com/EmilStenstrom/django-components/issues/818). +⚠️ Attention ⚠️ - Please update to v0.117 to fix known bugs. See [#791](https://github.com/django-components/django-components/issues/791) and [#789](https://github.com/django-components/django-components/issues/789) and [#818](https://github.com/django-components/django-components/issues/818). ### General @@ -174,7 +2079,7 @@ - Installation changes: - If your components include JS or CSS, you now must use the middleware and add django-components' URLs to your `urlpatterns` - (See "[Adding support for JS and CSS](https://github.com/EmilStenstrom/django-components#adding-support-for-js-and-css)") + (See "[Adding support for JS and CSS](https://github.com/django-components/django-components#adding-support-for-js-and-css)") - Component typing signature changed from @@ -225,7 +2130,7 @@ importing them. #### Refactor -- For advanced use cases, use can omit the middleware and instead manage component JS and CSS dependencies yourself with [`render_dependencies`](https://github.com/EmilStenstrom/django-components#render_dependencies-and-deep-dive-into-rendering-js--css-without-the-middleware) +- For advanced use cases, use can omit the middleware and instead manage component JS and CSS dependencies yourself with [`render_dependencies`](https://github.com/django-components/django-components#render_dependencies-and-deep-dive-into-rendering-js--css-without-the-middleware) - The [`ComponentRegistry`](../api#django_components.ComponentRegistry) settings [`RegistrySettings`](../api#django_components.RegistrySettings) were lowercased to align with the global settings: @@ -495,15 +2400,15 @@ importing them. - Installation changes: - - Instead of defining component directories in `STATICFILES_DIRS`, set them to [`COMPONENTS.dirs`](https://github.com/EmilStenstrom/django-components#dirs). + - Instead of defining component directories in `STATICFILES_DIRS`, set them to [`COMPONENTS.dirs`](https://github.com/django-components/django-components#dirs). - You now must define `STATICFILES_FINDERS` - - [See here how to migrate your settings.py](https://github.com/EmilStenstrom/django-components/blob/master/docs/migrating_from_safer_staticfiles.md) + - [See here how to migrate your settings.py](https://github.com/django-components/django-components/blob/master/docs/migrating_from_safer_staticfiles.md) #### Feat - Beside the top-level `/components` directory, you can now define also app-level components dirs, e.g. `[app]/components` - (See [`COMPONENTS.app_dirs`](https://github.com/EmilStenstrom/django-components#app_dirs)). + (See [`COMPONENTS.app_dirs`](https://github.com/django-components/django-components#app_dirs)). #### Refactor @@ -513,7 +2418,7 @@ importing them. #### Fix -- Fixed template caching. You can now also manually create cached templates with [`cached_template()`](https://github.com/EmilStenstrom/django-components#template_cache_size---tune-the-template-cache) +- Fixed template caching. You can now also manually create cached templates with [`cached_template()`](https://github.com/django-components/django-components#template_cache_size---tune-the-template-cache) #### Refactor @@ -528,9 +2433,9 @@ importing them. #### Feat -- Run-time type validation for Python 3.11+ - If the `Component` class is typed, e.g. `Component[Args, Kwargs, ...]`, the args, kwargs, slots, and data are validated against the given types. (See [Runtime input validation with types](https://github.com/EmilStenstrom/django-components#runtime-input-validation-with-types)) +- Run-time type validation for Python 3.11+ - If the `Component` class is typed, e.g. `Component[Args, Kwargs, ...]`, the args, kwargs, slots, and data are validated against the given types. (See [Runtime input validation with types](https://github.com/django-components/django-components#runtime-input-validation-with-types)) -- Render hooks - Set `on_render_before` and `on_render_after` methods on `Component` to intercept or modify the template or context before rendering, or the rendered result afterwards. (See [Component hooks](https://github.com/EmilStenstrom/django-components#component-hooks)) +- Render hooks - Set `on_render_before` and `on_render_after` methods on `Component` to intercept or modify the template or context before rendering, or the rendered result afterwards. (See [Component hooks](https://github.com/django-components/django-components#component-hooks)) - `component_vars.is_filled` context variable can be accessed from within `on_render_before` and `on_render_after` hooks as `self.is_filled.my_slot` @@ -538,7 +2443,7 @@ importing them. #### Feat -- Added support for dynamic components, where the component name is passed as a variable. (See [Dynamic components](https://github.com/EmilStenstrom/django-components#dynamic-components)) +- Added support for dynamic components, where the component name is passed as a variable. (See [Dynamic components](https://github.com/django-components/django-components#dynamic-components)) #### Refactor @@ -548,17 +2453,17 @@ importing them. #### Feat -- django_components now automatically configures Django to support multi-line tags. (See [Multi-line tags](https://github.com/EmilStenstrom/django-components#multi-line-tags)) +- django_components now automatically configures Django to support multi-line tags. (See [Multi-line tags](https://github.com/django-components/django-components#multi-line-tags)) -- New setting `reload_on_template_change`. Set this to `True` to reload the dev server on changes to component template files. (See [Reload dev server on component file changes](https://github.com/EmilStenstrom/django-components#reload-dev-server-on-component-file-changes)) +- New setting `reload_on_template_change`. Set this to `True` to reload the dev server on changes to component template files. (See [Reload dev server on component file changes](https://github.com/django-components/django-components#reload-dev-server-on-component-file-changes)) ## v0.93 #### Feat -- Spread operator `...dict` inside template tags. (See [Spread operator](https://github.com/EmilStenstrom/django-components#spread-operator)) +- Spread operator `...dict` inside template tags. (See [Spread operator](https://github.com/django-components/django-components#spread-operator)) -- Use template tags inside string literals in component inputs. (See [Use template tags inside component inputs](https://github.com/EmilStenstrom/django-components#use-template-tags-inside-component-inputs)) +- Use template tags inside string literals in component inputs. (See [Use template tags inside component inputs](https://github.com/django-components/django-components#use-template-tags-inside-component-inputs)) - Dynamic slots, fills and provides - The `name` argument for these can now be a variable, a template expression, or via spread operator @@ -568,13 +2473,13 @@ importing them. #### BREAKING CHANGES -- `Component` class is no longer a subclass of `View`. To configure the `View` class, set the `Component.View` nested class. HTTP methods like `get` or `post` can still be defined directly on `Component` class, and `Component.as_view()` internally calls `Component.View.as_view()`. (See [Modifying the View class](https://github.com/EmilStenstrom/django-components#modifying-the-view-class)) +- `Component` class is no longer a subclass of `View`. To configure the `View` class, set the `Component.View` nested class. HTTP methods like `get` or `post` can still be defined directly on `Component` class, and `Component.as_view()` internally calls `Component.View.as_view()`. (See [Modifying the View class](https://github.com/django-components/django-components#modifying-the-view-class)) #### Feat -- The inputs (args, kwargs, slots, context, ...) that you pass to `Component.render()` can be accessed from within `get_context_data`, `get_template` and `get_template_name` via `self.input`. (See [Accessing data passed to the component](https://github.com/EmilStenstrom/django-components#accessing-data-passed-to-the-component)) +- The inputs (args, kwargs, slots, context, ...) that you pass to `Component.render()` can be accessed from within `get_context_data`, `get_template` and `get_template_name` via `self.input`. (See [Accessing data passed to the component](https://github.com/django-components/django-components#accessing-data-passed-to-the-component)) -- Typing: `Component` class supports generics that specify types for `Component.render` (See [Adding type hints with Generics](https://github.com/EmilStenstrom/django-components#adding-type-hints-with-generics)) +- Typing: `Component` class supports generics that specify types for `Component.render` (See [Adding type hints with Generics](https://github.com/django-components/django-components#adding-type-hints-with-generics)) ## v0.90 @@ -595,7 +2500,7 @@ importing them. {% component "button" attrs:class="hidden" %} ``` -- You can change how the components are written in the template with [TagFormatter](https://github.com/EmilStenstrom/django-components#customizing-component-tags-with-tagformatter). +- You can change how the components are written in the template with [TagFormatter](https://github.com/django-components/django-components#customizing-component-tags-with-tagformatter). The default is `django_components.component_formatter`: @@ -628,7 +2533,7 @@ importing them. - `SETTINGS_MODULE` - Define component dirs using `STATICFILES_DIRS` - - Previously, autodiscovery handled relative files in `STATICFILES_DIRS`. To align with Django, `STATICFILES_DIRS` now must be full paths ([Django docs](https://docs.djangoproject.com/en/5.0/ref/settings/#std-setting-STATICFILES_DIRS)). + - Previously, autodiscovery handled relative files in `STATICFILES_DIRS`. To align with Django, `STATICFILES_DIRS` now must be full paths ([Django docs](https://docs.djangoproject.com/en/5.2/ref/settings/#std-setting-STATICFILES_DIRS)). ## 🚨📢 v0.81 @@ -652,7 +2557,7 @@ importing them. #### BREAKING CHANGES -- Default value for the `COMPONENTS.context_behavior` setting was changes from `"isolated"` to `"django"`. If you did not set this value explicitly before, this may be a breaking change. See the rationale for change [here](https://github.com/EmilStenstrom/django-components/issues/498). +- Default value for the `COMPONENTS.context_behavior` setting was changes from `"isolated"` to `"django"`. If you did not set this value explicitly before, this may be a breaking change. See the rationale for change [here](https://github.com/django-components/django-components/issues/498). ## 🚨📢 v0.77 @@ -689,13 +2594,13 @@ importing them. - `{% if_filled "my_slot" %}` tags were replaced with `{{ component_vars.is_filled.my_slot }}` variables. -- Simplified settings - `slot_context_behavior` and `context_behavior` were merged. See the [documentation](https://github.com/EmilStenstrom/django-components#context-behavior) for more details. +- Simplified settings - `slot_context_behavior` and `context_behavior` were merged. See the [documentation](https://github.com/django-components/django-components#context-behavior) for more details. ## v0.67 #### Refactor -- Changed the default way how context variables are resolved in slots. See the [documentation](https://github.com/EmilStenstrom/django-components/tree/0.67#isolate-components-slots) for more details. +- Changed the default way how context variables are resolved in slots. See the [documentation](https://github.com/django-components/django-components/tree/0.67#isolate-components-slots) for more details. ## 🚨📢 v0.50 @@ -711,7 +2616,7 @@ importing them. #### Feat -- Components as views, which allows you to handle requests and render responses from within a component. See the [documentation](https://github.com/EmilStenstrom/django-components#use-components-as-views) for more details. +- Components as views, which allows you to handle requests and render responses from within a component. See the [documentation](https://github.com/django-components/django-components#use-components-as-views) for more details. ## v0.28 @@ -723,7 +2628,7 @@ importing them. #### Feat -- A second installable app `django_components.safer_staticfiles`. It provides the same behavior as `django.contrib.staticfiles` but with extra security guarantees (more info below in [Security Notes](https://github.com/EmilStenstrom/django-components#security-notes)). +- A second installable app `django_components.safer_staticfiles`. It provides the same behavior as `django.contrib.staticfiles` but with extra security guarantees (more info below in [Security Notes](https://github.com/django-components/django-components#security-notes)). ## 🚨📢 v0.26 diff --git a/README.md b/README.md index 72de59df..8df3262e 100644 --- a/README.md +++ b/README.md @@ -1,77 +1,565 @@ -# django-components +# django-components -[![PyPI - Version](https://img.shields.io/pypi/v/django-components)](https://pypi.org/project/django-components/) [![PyPI - Python Version](https://img.shields.io/pypi/pyversions/django-components)](https://pypi.org/project/django-components/) [![PyPI - License](https://img.shields.io/pypi/l/django-components)](https://github.com/EmilStenstrom/django-components/blob/master/LICENSE/) [![PyPI - Downloads](https://img.shields.io/pypi/dm/django-components)](https://pypistats.org/packages/django-components) [![GitHub Actions Workflow Status](https://img.shields.io/github/actions/workflow/status/EmilStenstrom/django-components/tests.yml)](https://github.com/EmilStenstrom/django-components/actions/workflows/tests.yml) +[![PyPI - Version](https://img.shields.io/pypi/v/django-components)](https://pypi.org/project/django-components/) [![PyPI - Python Version](https://img.shields.io/pypi/pyversions/django-components)](https://pypi.org/project/django-components/) [![PyPI - License](https://img.shields.io/pypi/l/django-components)](https://github.com/django-components/django-components/blob/master/LICENSE/) [![PyPI - Downloads](https://img.shields.io/pypi/dm/django-components)](https://pypistats.org/packages/django-components) [![GitHub Actions Workflow Status](https://img.shields.io/github/actions/workflow/status/django-components/django-components/tests.yml)](https://github.com/django-components/django-components/actions/workflows/tests.yml) [![asv](https://img.shields.io/badge/benchmarked%20by-asv-blue.svg?style=flat)](https://django-components.github.io/django-components/latest/benchmarks/) -[**Docs (Work in progress)**](https://EmilStenstrom.github.io/django-components/latest/) +###
[Read the full documentation](https://django-components.github.io/django-components/latest/)
-Django-components is a package that introduces component-based architecture to Django's server-side rendering. It aims to combine Django's templating system with the modularity seen in modern frontend frameworks. +`django-components` is a modular and extensible UI framework for Django. -## Features +It combines Django's templating system with the modularity seen +in modern frontend frameworks like Vue or React. -1. 🧩 **Reusability:** Allows creation of self-contained, reusable UI elements. -2. 📦 **Encapsulation:** Each component can include its own HTML, CSS, and JavaScript. -3. 🚀 **Server-side rendering:** Components render on the server, improving initial load times and SEO. -4. 🐍 **Django integration:** Works within the Django ecosystem, using familiar concepts like template tags. -5. ⚡ **Asynchronous loading:** Components can render independently opening up for integration with JS frameworks like HTMX or AlpineJS. - -Potential benefits: - -- 🔄 Reduced code duplication -- 🛠️ Improved maintainability through modular design -- 🧠 Easier management of complex UIs -- 🤝 Enhanced collaboration between frontend and backend developers - -Django-components can be particularly useful for larger Django projects that require a more structured approach to UI development, without necessitating a shift to a separate frontend framework. +With `django-components` you can support Django projects small and large without leaving the Django ecosystem. ## Quickstart -django-components lets you create reusable blocks of code needed to generate the front end code you need for a modern app. +A component in django-components can be as simple as a Django template and Python code to declare the component: -Define a component in `components/calendar/calendar.py` like this: -```python -@register("calendar") -class Calendar(Component): - template_name = "template.html" - - def get_context_data(self, date): - return {"date": date} +```django +{# components/calendar/calendar.html #} +
+ Today's date is {{ date }} +
``` -With this `template.html` file: +```py +# components/calendar/calendar.py +from django_components import Component, register -```htmldjango -
Today's date is {{ date }}
+@register("calendar") +class Calendar(Component): + template_file = "calendar.html" +``` + +Or a combination of Django template, Python, CSS, and Javascript: + +```django +{# components/calendar/calendar.html #} +
+ Today's date is {{ date }} +
+``` + +```css +/* components/calendar/calendar.css */ +.calendar { + width: 200px; + background: pink; +} +``` + +```js +/* components/calendar/calendar.js */ +document.querySelector(".calendar").onclick = () => { + alert("Clicked calendar!"); +}; +``` + +```py +# components/calendar/calendar.py +from django_components import Component, register + +@register("calendar") +class Calendar(Component): + template_file = "calendar.html" + js_file = "calendar.js" + css_file = "calendar.css" + + def get_template_data(self, args, kwargs, slots, context): + return {"date": kwargs["date"]} ``` Use the component like this: -```htmldjango +```django {% component "calendar" date="2024-11-06" %}{% endcomponent %} ``` And this is what gets rendered: ```html -
Today's date is 2024-11-06
+
+ Today's date is 2024-11-06 +
``` Read on to learn about all the exciting details and configuration possibilities! -(If you instead prefer to jump right into the code, [check out the example project](https://github.com/EmilStenstrom/django-components/tree/master/sampleproject)) +(If you instead prefer to jump right into the code, [check out the example project](https://github.com/django-components/django-components/tree/master/sampleproject)) + +## Features + +### Modern and modular UI + +- Create self-contained, reusable UI elements. +- Each component can include its own HTML, CSS, and JS, or additional third-party JS and CSS. +- HTML, CSS, and JS can be defined on the component class, or loaded from files. + +```python +from django_components import Component + +@register("calendar") +class Calendar(Component): + template = """ +
+ Today's date is + {{ date }} +
+ """ + + css = """ + .calendar { + width: 200px; + background: pink; + } + """ + + js = """ + document.querySelector(".calendar") + .addEventListener("click", () => { + alert("Clicked calendar!"); + }); + """ + + # Additional JS and CSS + class Media: + js = ["https://cdn.jsdelivr.net/npm/htmx.org@2/dist/htmx.min.js"] + css = ["bootstrap/dist/css/bootstrap.min.css"] + + # Variables available in the template + def get_template_data(self, args, kwargs, slots, context): + return { + "date": kwargs["date"] + } +``` + +### Composition with slots + +- Render components inside templates with + [`{% component %}`](https://django-components.github.io/django-components/latest/reference/template_tags#component) tag. +- Compose them with [`{% slot %}`](https://django-components.github.io/django-components/latest/reference/template_tags#slot) + and [`{% fill %}`](https://django-components.github.io/django-components/latest/reference/template_tags#fill) tags. +- Vue-like slot system, including [scoped slots](https://django-components.github.io/django-components/latest/concepts/fundamentals/slots/#slot-data). + +```django +{% component "Layout" + bookmarks=bookmarks + breadcrumbs=breadcrumbs +%} + {% fill "header" %} +
+
+

{{ project.name }}

+
+
+ {{ project.start_date }} - {{ project.end_date }} +
+
+ {% endfill %} + + {# Access data passed to `{% slot %}` with `data` #} + {% fill "tabs" data="tabs_data" %} + {% component "TabItem" header="Project Info" %} + {% component "ProjectInfo" + project=project + project_tags=project_tags + attrs:class="py-5" + attrs:width=tabs_data.width + / %} + {% endcomponent %} + {% endfill %} +{% endcomponent %} +``` + +### Extended template tags + +`django-components` is designed for flexibility, making working with templates a breeze. + +It extends Django's template tags syntax with: + + +- Literal lists and dictionaries in the template +- [Self-closing tags](https://django-components.github.io/django-components/latest/concepts/fundamentals/template_tag_syntax#self-closing-tags) `{% mytag / %}` +- [Multi-line template tags](https://django-components.github.io/django-components/latest/concepts/fundamentals/template_tag_syntax#multiline-tags) +- [Spread operator](https://django-components.github.io/django-components/latest/concepts/fundamentals/template_tag_syntax#spread-operator) `...` to dynamically pass args or kwargs into the template tag +- [Template tags inside literal strings](https://django-components.github.io/django-components/latest/concepts/fundamentals/template_tag_syntax#template-tags-inside-literal-strings) like `"{{ first_name }} {{ last_name }}"` +- [Pass dictonaries by their key-value pairs](https://django-components.github.io/django-components/latest/concepts/fundamentals/template_tag_syntax#pass-dictonary-by-its-key-value-pairs) `attr:key=val` + +```django +{% component "table" + ...default_attrs + title="Friend list for {{ user.name }}" + headers=["Name", "Age", "Email"] + data=[ + { + "name": "John"|upper, + "age": 30|add:1, + "email": "john@example.com", + "hobbies": ["reading"], + }, + { + "name": "Jane"|upper, + "age": 25|add:1, + "email": "jane@example.com", + "hobbies": ["reading", "coding"], + }, + ], + attrs:class="py-4 ma-2 border-2 border-gray-300 rounded-md" +/ %} +``` + +You too can define template tags with these features by using +[`@template_tag()`](https://django-components.github.io/django-components/latest/reference/api/#django_components.template_tag) +or [`BaseNode`](https://django-components.github.io/django-components/latest/reference/api/#django_components.BaseNode). + +Read more on [Custom template tags](https://django-components.github.io/django-components/latest/concepts/advanced/template_tags/). + +### Full programmatic access + +When you render a component, you can access everything about the component: + +- Component input: [args, kwargs, slots and context](https://django-components.github.io/django-components/latest/concepts/fundamentals/render_api/#component-inputs) +- Component's template, CSS and JS +- Django's [context processors](https://django-components.github.io/django-components/latest/concepts/fundamentals/render_api/#request-and-context-processors) +- Unique [render ID](https://django-components.github.io/django-components/latest/concepts/fundamentals/render_api/#component-id) + +```python +class Table(Component): + js_file = "table.js" + css_file = "table.css" + + template = """ +
+ {{ variable }} +
+ """ + + def get_template_data(self, args, kwargs, slots, context): + # Access component's ID + assert self.id == "djc1A2b3c" + + # Access component's inputs and slots + assert self.args == [123, "str"] + assert self.kwargs == {"variable": "test", "another": 1} + footer_slot = self.slots["footer"] + some_var = self.context["some_var"] + + # Access the request object and Django's context processors, if available + assert self.request.GET == {"query": "something"} + assert self.context_processors_data['user'].username == "admin" + + return { + "variable": kwargs["variable"], + } + +# Access component's HTML / JS / CSS +Table.template +Table.js +Table.css + +# Render the component +rendered = Table.render( + kwargs={"variable": "test", "another": 1}, + args=(123, "str"), + slots={"footer": "MY_FOOTER"}, +) +``` + +### Granular HTML attributes + +Use the [`{% html_attrs %}`](https://django-components.github.io/django-components/latest/concepts/fundamentals/html_attributes/) template tag to render HTML attributes. + +It supports: + +- Defining attributes as whole dictionaries or keyword arguments +- Merging attributes from multiple sources +- Boolean attributes +- Appending attributes +- Removing attributes +- Defining default attributes + +```django +
+``` + +[`{% html_attrs %}`](https://django-components.github.io/django-components/latest/concepts/fundamentals/html_attributes/) offers a Vue-like granular control for +[`class`](https://django-components.github.io/django-components/latest/concepts/fundamentals/html_attributes/#merging-class-attributes) +and [`style`](https://django-components.github.io/django-components/latest/concepts/fundamentals/html_attributes/#merging-style-attributes) +HTML attributes, +where you can use a dictionary to manage each class name or style property separately. + +```django +{% html_attrs + class="foo bar" + class={ + "baz": True, + "foo": False, + } + class="extra" +%} +``` + +```django +{% html_attrs + style="text-align: center; background-color: blue;" + style={ + "background-color": "green", + "color": None, + "width": False, + } + style="position: absolute; height: 12px;" +%} +``` + +Read more about [HTML attributes](https://django-components.github.io/django-components/latest/concepts/fundamentals/html_attributes/). + +### HTML fragment support + +`django-components` makes integration with HTMX, AlpineJS or jQuery easy by allowing components to be rendered as [HTML fragments](https://django-components.github.io/django-components/latest/concepts/advanced/html_fragments/): + +- Components's JS and CSS files are loaded automatically when the fragment is inserted into the DOM. + +- Components can be [exposed as Django Views](https://django-components.github.io/django-components/latest/concepts/fundamentals/component_views_urls/) with `get()`, `post()`, `put()`, `patch()`, `delete()` methods + +- Automatically create an endpoint for a component with [`Component.View.public`](https://django-components.github.io/django-components/latest/concepts/fundamentals/component_views_urls/#register-urls-automatically) + +```py +# components/calendar/calendar.py +@register("calendar") +class Calendar(Component): + template_file = "calendar.html" + + class View: + # Register Component with `urlpatterns` + public = True + + # Define handlers + def get(self, request, *args, **kwargs): + page = request.GET.get("page", 1) + return self.component.render_to_response( + request=request, + kwargs={ + "page": page, + }, + ) + + def get_template_data(self, args, kwargs, slots, context): + return { + "page": kwargs["page"], + } + +# Get auto-generated URL for the component +url = get_component_url(Calendar) + +# Or define explicit URL in urls.py +path("calendar/", Calendar.as_view()) +``` + +### Provide / Inject + +`django-components` supports the provide / inject pattern, similarly to React's [Context Providers](https://react.dev/learn/passing-data-deeply-with-context) or Vue's [provide / inject](https://vuejs.org/guide/components/provide-inject): + +- Use the [`{% provide %}`](https://django-components.github.io/django-components/latest/reference/template_tags/#provide) tag to provide data to the component tree +- Use the [`Component.inject()`](https://django-components.github.io/django-components/latest/reference/api/#django_components.Component.inject) method to inject data into the component + +Read more about [Provide / Inject](https://django-components.github.io/django-components/latest/concepts/advanced/provide_inject). + +```django + + {% provide "theme" variant="light" %} + {% component "header" / %} + {% endprovide %} + +``` + +```djc_py +@register("header") +class Header(Component): + template = "..." + + def get_template_data(self, args, kwargs, slots, context): + theme = self.inject("theme").variant + return { + "theme": theme, + } +``` + +### Input validation and static type hints + +Avoid needless errors with [type hints and runtime input validation](https://django-components.github.io/django-components/latest/concepts/fundamentals/typing_and_validation/). + +To opt-in to input validation, define types for component's args, kwargs, slots, and more: + +```py +from typing import NamedTuple, Optional +from django.template import Context +from django_components import Component, Slot, SlotInput + +class Button(Component): + class Args(NamedTuple): + size: int + text: str + + class Kwargs(NamedTuple): + variable: str + another: int + maybe_var: Optional[int] = None # May be omitted + + class Slots(NamedTuple): + my_slot: Optional[SlotInput] = None + another_slot: SlotInput + + def get_template_data(self, args: Args, kwargs: Kwargs, slots: Slots, context: Context): + args.size # int + kwargs.variable # str + slots.my_slot # Slot[MySlotData] +``` + +To have type hints when calling +[`Button.render()`](https://django-components.github.io/django-components/latest/reference/api/#django_components.Component.render) or +[`Button.render_to_response()`](https://django-components.github.io/django-components/latest/reference/api/#django_components.Component.render_to_response), +wrap the inputs in their respective `Args`, `Kwargs`, and `Slots` classes: + +```py +Button.render( + # Error: First arg must be `int`, got `float` + args=Button.Args( + size=1.25, + text="abc", + ), + # Error: Key "another" is missing + kwargs=Button.Kwargs( + variable="text", + ), +) +``` + +### Extensions + +Django-components functionality can be extended with [Extensions](https://django-components.github.io/django-components/latest/concepts/advanced/extensions/). +Extensions allow for powerful customization and integrations. They can: + +- Tap into lifecycle events, such as when a component is created, deleted, or registered +- Add new attributes and methods to the components +- Add custom CLI commands +- Add custom URLs + +Some of the extensions include: + +- [Component caching](https://github.com/django-components/django-components/blob/master/src/django_components/extensions/cache.py) +- [Django View integration](https://github.com/django-components/django-components/blob/master/src/django_components/extensions/view.py) +- [Component defaults](https://github.com/django-components/django-components/blob/master/src/django_components/extensions/defaults.py) +- [Pydantic integration (input validation)](https://github.com/django-components/djc-ext-pydantic) + +Some of the planned extensions include: + +- AlpineJS integration +- Storybook integration +- Component-level benchmarking with asv + +### Caching + +- [Components can be cached](https://django-components.github.io/django-components/latest/concepts/advanced/component_caching/) using Django's cache framework. +- Caching rules can be configured on a per-component basis. +- Components are cached based on their input. Or you can write custom caching logic. + +```py +from django_components import Component + +class MyComponent(Component): + class Cache: + enabled = True + ttl = 60 * 60 * 24 # 1 day + + def hash(self, *args, **kwargs): + return hash(f"{json.dumps(args)}:{json.dumps(kwargs)}") +``` + +### Simple testing + +- Write tests for components with [`@djc_test`](https://django-components.github.io/django-components/latest/concepts/advanced/testing/) decorator. +- The decorator manages global state, ensuring that tests don't leak. +- If using `pytest`, the decorator allows you to parametrize Django or Components settings. +- The decorator also serves as a stand-in for Django's [`@override_settings`](https://docs.djangoproject.com/en/5.2/topics/testing/tools/#django.test.override_settings). + +```python +from django_components.testing import djc_test + +from components.my_table import MyTable + +@djc_test +def test_my_table(): + rendered = MyTable.render( + kwargs={ + "title": "My table", + }, + ) + assert rendered == "My table
" +``` + +### Debugging features + +- **Visual component inspection**: Highlight components and slots directly in your browser. +- **Detailed tracing logs to supply AI-agents with context**: The logs include component and slot names and IDs, and their position in the tree. + +
+Component debugging visualization showing slot highlighting +
+ +### Sharing components + +- Install and use third-party components from PyPI +- Or publish your own "component registry" +- Highly customizable - Choose how the components are called in the template (and more): + + ```django + {% component "calendar" date="2024-11-06" %} + {% endcomponent %} + + {% calendar date="2024-11-06" %} + {% endcalendar %} + ``` + +## Documentation + +[Read the full documentation here](https://django-components.github.io/django-components/latest/). + +... or jump right into the code, [check out the example project](https://github.com/django-components/django-components/tree/master/sampleproject). + +## Performance + +Our aim is to be at least as fast as Django templates. + +As of `0.130`, `django-components` is ~4x slower than Django templates. + +| | Render time| +|----------|----------------------| +| django | 68.9±0.6ms | +| django-components | 259±4ms | + +See the [full performance breakdown](https://django-components.github.io/django-components/latest/benchmarks/) for more information. ## Release notes -Read the [Release Notes](https://github.com/EmilStenstrom/django-components/tree/master/CHANGELOG.md) +Read the [Release Notes](https://github.com/django-components/django-components/tree/master/CHANGELOG.md) to see the latest features and fixes. ## Community examples One of our goals with `django-components` is to make it easy to share components between projects. If you have a set of components that you think would be useful to others, please open a pull request to add them to the list below. -- [django-htmx-components](https://github.com/iwanalabs/django-htmx-components): A set of components for use with [htmx](https://htmx.org/). Try out the [live demo](https://dhc.iwanalabs.com/). +- [django-htmx-components](https://github.com/iwanalabs/django-htmx-components): A set of components for use with [htmx](https://htmx.org/). + +- [djc-heroicons](https://pypi.org/project/djc-heroicons/): A component that renders icons from [Heroicons.com](https://heroicons.com/). ## Contributing and development -Get involved or sponsor this project - [See here](https://emilstenstrom.github.io/django-components/dev/overview/contributing/) +Get involved or sponsor this project - [See here](https://django-components.github.io/django-components/dev/overview/contributing/) -Running django-components locally for development - [See here](https://emilstenstrom.github.io/django-components/dev/overview/development/) +Running django-components locally for development - [See here](https://django-components.github.io/django-components/dev/overview/development/) diff --git a/asv.conf.json b/asv.conf.json new file mode 100644 index 00000000..0ae16e29 --- /dev/null +++ b/asv.conf.json @@ -0,0 +1,210 @@ +{ + // The version of the config file format. Do not change, unless + // you know what you are doing + "version": 1, + + // The name of the project being benchmarked + "project": "django-components", + + // The project's homepage + // "project_url": "https://django-components.github.io/django-components/", + "project_url": "/django-components/", // Relative path, since benchmarks are nested under the docs site + + // The URL or local path of the source code repository for the + // project being benchmarked + "repo": ".", + + // The Python project's subdirectory in your repo. If missing or + // the empty string, the project is assumed to be located at the root + // of the repository. + // "repo_subdir": "", + + // Customizable commands for building the project. + // See asv.conf.json documentation. + // To build the package using pyproject.toml (PEP518), uncomment the following lines + // "build_command": [ + // "python -m pip install build", + // "python -m build", + // "python -mpip wheel -w {build_cache_dir} {build_dir}" + // ], + // To build the package using setuptools and a setup.py file, uncomment the following lines + // "build_command": [ + // "python setup.py build", + // "python -mpip wheel -w {build_cache_dir} {build_dir}" + // ], + + // Customizable commands for installing and uninstalling the project. + // See asv.conf.json documentation. + // "install_command": ["in-dir={env_dir} python -mpip install {wheel_file}"], + // "uninstall_command": ["return-code=any python -mpip uninstall -y {project}"], + "install_command": ["in-dir={env_dir} python -mpip install ./project"], + + // List of branches to benchmark. If not provided, defaults to "main" + // (for git) or "default" (for mercurial). + // "branches": ["main"], // for git + // "branches": ["default"], // for mercurial + "branches": [ + "master" + ], + + // The DVCS being used. If not set, it will be automatically + // determined from "repo" by looking at the protocol in the URL + // (if remote), or by looking for special directories, such as + // ".git" (if local). + // "dvcs": "git", + + // The tool to use to create environments. May be "conda", + // "virtualenv", "mamba" (above 3.8) + // or other value depending on the plugins in use. + // If missing or the empty string, the tool will be automatically + // determined by looking for tools on the PATH environment + // variable. + "environment_type": "virtualenv", + + // timeout in seconds for installing any dependencies in environment + // defaults to 10 min + //"install_timeout": 600, + + // the base URL to show a commit for the project. + // "show_commit_url": "http://github.com/owner/project/commit/", + + // The Pythons you'd like to test against. If not provided, defaults + // to the current version of Python used to run `asv`. + "pythons": [ + "3.13" + ], + + // The list of conda channel names to be searched for benchmark + // dependency packages in the specified order + // "conda_channels": ["conda-forge", "defaults"], + + // A conda environment file that is used for environment creation. + // "conda_environment_file": "environment.yml", + + // The matrix of dependencies to test. Each key of the "req" + // requirements dictionary is the name of a package (in PyPI) and + // the values are version numbers. An empty list or empty string + // indicates to just test against the default (latest) + // version. null indicates that the package is to not be + // installed. If the package to be tested is only available from + // PyPi, and the 'environment_type' is conda, then you can preface + // the package name by 'pip+', and the package will be installed + // via pip (with all the conda available packages installed first, + // followed by the pip installed packages). + // + // The ``@env`` and ``@env_nobuild`` keys contain the matrix of + // environment variables to pass to build and benchmark commands. + // An environment will be created for every combination of the + // cartesian product of the "@env" variables in this matrix. + // Variables in "@env_nobuild" will be passed to every environment + // during the benchmark phase, but will not trigger creation of + // new environments. A value of ``null`` means that the variable + // will not be set for the current combination. + // + // "matrix": { + // "req": { + // "numpy": ["1.6", "1.7"], + // "six": ["", null], // test with and without six installed + // "pip+emcee": [""] // emcee is only available for install with pip. + // }, + // "env": {"ENV_VAR_1": ["val1", "val2"]}, + // "env_nobuild": {"ENV_VAR_2": ["val3", null]}, + // }, + "matrix": { + "req": { + "django": [ + "5.1" + ], + "djc-core-html-parser": [""] // Empty string means the latest version + } + }, + + // Combinations of libraries/python versions can be excluded/included + // from the set to test. Each entry is a dictionary containing additional + // key-value pairs to include/exclude. + // + // An exclude entry excludes entries where all values match. The + // values are regexps that should match the whole string. + // + // An include entry adds an environment. Only the packages listed + // are installed. The 'python' key is required. The exclude rules + // do not apply to includes. + // + // In addition to package names, the following keys are available: + // + // - python + // Python version, as in the *pythons* variable above. + // - environment_type + // Environment type, as above. + // - sys_platform + // Platform, as in sys.platform. Possible values for the common + // cases: 'linux2', 'win32', 'cygwin', 'darwin'. + // - req + // Required packages + // - env + // Environment variables + // - env_nobuild + // Non-build environment variables + // + // "exclude": [ + // {"python": "3.2", "sys_platform": "win32"}, // skip py3.2 on windows + // {"environment_type": "conda", "req": {"six": null}}, // don't run without six on conda + // {"env": {"ENV_VAR_1": "val2"}}, // skip val2 for ENV_VAR_1 + // ], + // + // "include": [ + // // additional env for python3.12 + // {"python": "3.12", "req": {"numpy": "1.26"}, "env_nobuild": {"FOO": "123"}}, + // // additional env if run on windows+conda + // {"platform": "win32", "environment_type": "conda", "python": "3.12", "req": {"libpython": ""}}, + // ], + + // The directory (relative to the current directory) that benchmarks are + // stored in. If not provided, defaults to "benchmarks" + "benchmark_dir": "benchmarks", + + // The directory (relative to the current directory) to cache the Python + // environments in. If not provided, defaults to "env" + "env_dir": ".asv/env", + + // The directory (relative to the current directory) that raw benchmark + // results are stored in. If not provided, defaults to "results". + "results_dir": ".asv/results", + + // The directory (relative to the current directory) that the html tree + // should be written to. If not provided, defaults to "html". + // "html_dir": ".asv/html", + "html_dir": "docs/benchmarks", // # TODO + + // The number of characters to retain in the commit hashes. + // "hash_length": 8, + + // `asv` will cache results of the recent builds in each + // environment, making them faster to install next time. This is + // the number of builds to keep, per environment. + // "build_cache_size": 2, + + // The commits after which the regression search in `asv publish` + // should start looking for regressions. Dictionary whose keys are + // regexps matching to benchmark names, and values corresponding to + // the commit (exclusive) after which to start looking for + // regressions. The default is to start from the first commit + // with results. If the commit is `null`, regression detection is + // skipped for the matching benchmark. + // + // "regressions_first_commits": { + // "some_benchmark": "352cdf", // Consider regressions only after this commit + // "another_benchmark": null, // Skip regression detection altogether + // }, + + // The thresholds for relative change in results, after which `asv + // publish` starts reporting regressions. Dictionary of the same + // form as in ``regressions_first_commits``, with values + // indicating the thresholds. If multiple entries match, the + // maximum is taken. If no entry matches, the default is 5%. + // + // "regressions_thresholds": { + // "some_benchmark": 0.01, // Threshold of 1% + // "another_benchmark": 0.5, // Threshold of 50% + // }, +} diff --git a/benchmarks/README.md b/benchmarks/README.md new file mode 100644 index 00000000..f5f5d524 --- /dev/null +++ b/benchmarks/README.md @@ -0,0 +1,195 @@ +# Benchmarks + +## Overview + +[`asv`](https://github.com/airspeed-velocity/) (Airspeed Velocity) is used for benchmarking performance. + +`asv` covers the entire benchmarking workflow. We can: + +1. Define benchmark tests similarly to writing pytest tests (supports both timing and memory benchmarks) +2. Run the benchmarks and generate results for individual git commits, tags, or entire branches +3. View results as an HTML report (dashboard with charts) +4. Compare performance between two commits / tags / branches for CI integration + +![asv dashboard](./assets/asv_dashboard.png) + +django-components uses `asv` for these use cases: + +- Benchmarking across releases: + + 1. When a git tag is created and pushed, this triggers a Github Action workflow (see `docs.yml`). + 2. The workflow runs the benchmarks with the latest release, and commits the results to the repository. + Thus, we can see how performance changes across releases. + +- Displaying performance results on the website: + + 1. When a git tag is created and pushed, we also update the documentation website (see `docs.yml`). + 2. Before we publish the docs website, we generate the HTML report for the benchmark results. + 3. The generated report is placed in the `docs/benchmarks/` directory, and is thus + published with the rest of the docs website and available under [`/benchmarks/`](https://django-components.github.io/django-components/latest/benchmarks). + - NOTE: The location where the report is placed is defined in `asv.conf.json`. + +- Compare performance between commits on pull requests: + 1. When a pull request is made, this triggers a Github Action workflow (see `benchmark.yml`). + 2. The workflow compares performance between commits. + 3. The report is added to the PR as a comment made by a bot. + +## Interpreting benchmarks + +The results CANNOT be taken as ABSOLUTE values e.g.: + +"This example took 200ms to render, so my page will also take 200ms to render." + +Each UI may consist of different number of Django templates, template tags, and components, and all these may influence the rendering time differently. + +Instead, the results MUST be understood as RELATIVE values. + +- If a commit is 10% slower than the master branch, that's valid. +- If Django components are 10% slower than vanilla Django templates, that's valid. +- If "isolated" mode is 10% slower than "django" mode, that's valid. + +## Development + +Let's say we want to generate results for the last 5 commits. + +1. Install `asv` + + ```bash + pip install asv + ``` + +2. Run benchmarks and generate results + + ```bash + asv run HEAD --steps 5 -e + ``` + + - `HEAD` means that we want to run benchmarks against the [current branch](https://stackoverflow.com/a/2304106/9788634). + - `--steps 5` means that we want to run benchmarks for the last 5 commits. + - `-e` to print out any errors. + + The results will be stored in `.asv/results/`, as configured in `asv.conf.json`. + +3. Generate HTML report + + ```bash + asv publish + asv preview + ``` + + - `publish` generates the HTML report and stores it in `docs/benchmarks/`, as configured in `asv.conf.json`. + - `preview` starts a local server and opens the report in the browser. + + NOTE: Since the results are stored in `docs/benchmarks/`, you can also view the results + with `mkdocs serve` and navigating to `http://localhost:9000/django-components/benchmarks/`. + + NOTE 2: Running `publish` will overwrite the existing contents of `docs/benchmarks/`. + +## Writing benchmarks + +`asv` supports writing different [types of benchmarks](https://asv.readthedocs.io/en/latest/writing_benchmarks.html#benchmark-types). What's relevant for us is: + +- [Raw timing benchmarks](https://asv.readthedocs.io/en/latest/writing_benchmarks.html#raw-timing-benchmarks) +- [Peak memory benchmarks](https://asv.readthedocs.io/en/latest/writing_benchmarks.html#peak-memory) + +Notes: + +- The difference between "raw timing" and "timing" tests is that "raw timing" is ran in a separate process. + And instead of running the logic within the test function itself, we return a script (string) + that will be executed in the separate process. + +- The difference between "peak memory" and "memory" tests is that "memory" calculates the memory + of the object returned from the test function. On the other hand, "peak memory" detects the + peak memory usage during the execution of the test function (including the setup function). + +You can write the test file anywhere in the `benchmarks/` directory, `asv` will automatically find it. + +Inside the file, write a test function. Depending on the type of the benchmark, +prefix the test function name with `timeraw_` or `peakmem_`. See [`benchmarks/benchmark_templating.py`](benchmark_templating.py) for examples. + +### Ensuring that the benchmarked logic is correct + +The approach I (Juro) took with benchmarking the overall template rendering is that +I've defined the actual logic in `tests/test_benchmark_*.py` files. So those files +are part of the normal pytest testing, and even contain a section with pytest tests. + +This ensures that the benchmarked logic remains functional and error-free. + +However, there's some caveats: + +1. I wasn't able to import files from `tests/`. +2. When running benchmarks, we don't want to run the pytest tests. + +To work around that, the approach I used for loading the files from the `tests/` directory is to: + +1. Get the file's source code as a string. +2. Cut out unwanted sections (like the pytest tests). +3. Append the benchmark-specific code to the file (e.g. to actually render the templates). +4. In case of "timeraw" benchmarks, we can simply return the remaining code as a string + to be run in a separate process. +5. In case of "peakmem" benchmarks, we need to access this modified source code as Python objects. + So the code is made available as a "virtual" module, which makes it possible to import Python objects like so: + ```py + from my_virtual_module import run_my_benchmark + ``` + +## Using `asv` + +### Compare latest commit against master + +Note: Before comparing, you must run the benchmarks first to generate the results. The `continuous` command does not generate the results by itself. + +```bash +asv continuous master^! HEAD^! --factor 1.1 +``` + +- Factor of `1.1` means that the new commit is allowed to be 10% slower/faster than the master commit. + +- `^` means that we mean the COMMIT of the branch, not the BRANCH itself. + + Without it, we would run benchmarks for the whole branch history. + + With it, we run benchmarks FROM the latest commit (incl) TO ... + +- `!` means that we want to select range spanning a single commit. + + Without it, we would run benchmarks for all commits FROM the latest commit + TO the start of the branch history. + + With it, we run benchmarks ONLY FOR the latest commit. + +### More Examples + +Notes: + +- Use `~1` to select the second-latest commit, `~2` for the third-latest, etc.. + +Generate benchmarks for the latest commit in `master` branch. + +```bash +asv run master^! +``` + +Generate benchmarks for second-latest commit in `master` branch. + +```bash +asv run master~1^! +``` + +Generate benchmarks for all commits in `master` branch. + +```bash +asv run master +``` + +Generate benchmarks for all commits in `master` branch, but exclude the latest commit. + +```bash +asv run master~1 +``` + +Generate benchmarks for the LAST 5 commits in `master` branch, but exclude the latest commit. + +```bash +asv run master~1 --steps 5 +``` diff --git a/benchmarks/__init__.py b/benchmarks/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/benchmarks/assets/asv_dashboard.png b/benchmarks/assets/asv_dashboard.png new file mode 100644 index 00000000..524990b8 Binary files /dev/null and b/benchmarks/assets/asv_dashboard.png differ diff --git a/benchmarks/benchmark_templating.py b/benchmarks/benchmark_templating.py new file mode 100644 index 00000000..779c70aa --- /dev/null +++ b/benchmarks/benchmark_templating.py @@ -0,0 +1,446 @@ +# Write the benchmarking functions here +# See "Writing benchmarks" in the asv docs for more information. + +import re +from pathlib import Path +from types import ModuleType +from typing import Literal + +# Fix for for https://github.com/airspeed-velocity/asv_runner/pull/44 +import benchmarks.monkeypatch_asv # noqa: F401 + +from benchmarks.utils import benchmark, create_virtual_module + + +DJC_VS_DJ_GROUP = "Components vs Django" +DJC_ISOLATED_VS_NON_GROUP = "isolated vs django modes" +OTHER_GROUP = "Other" + + +DjcContextMode = Literal["isolated", "django"] +TemplatingRenderer = Literal["django", "django-components", "none"] +TemplatingTestSize = Literal["lg", "sm"] +TemplatingTestType = Literal[ + "first", # Testing performance of the first time the template is rendered + "subsequent", # Testing performance of the subsequent times the template is rendered + "startup", # Testing performance of the startup time (e.g. defining classes and templates) +] + + +def _get_templating_filepath(renderer: TemplatingRenderer, size: TemplatingTestSize) -> Path: + if renderer == "none": + raise ValueError("Cannot get filepath for renderer 'none'") + elif renderer not in ["django", "django-components"]: + raise ValueError(f"Invalid renderer: {renderer}") + + if size not in ("lg", "sm"): + raise ValueError(f"Invalid size: {size}, must be one of ('lg', 'sm')") + + # At this point, we know the renderer is either "django" or "django-components" + root = file_path = Path(__file__).parent.parent + if renderer == "django": + if size == "lg": + file_path = root / "tests" / "test_benchmark_django.py" + else: + file_path = root / "tests" / "test_benchmark_django_small.py" + else: + if size == "lg": + file_path = root / "tests" / "test_benchmark_djc.py" + else: + file_path = root / "tests" / "test_benchmark_djc_small.py" + + return file_path + + +def _get_templating_script( + renderer: TemplatingRenderer, + size: TemplatingTestSize, + context_mode: DjcContextMode, + imports_only: bool, +) -> str: + if renderer == "none": + return "" + elif renderer not in ["django", "django-components"]: + raise ValueError(f"Invalid renderer: {renderer}") + + # At this point, we know the renderer is either "django" or "django-components" + file_path = _get_templating_filepath(renderer, size) + contents = file_path.read_text() + + # The files with benchmarked code also have a section for testing them with pytest. + # We remove that pytest section, so the script is only the benchmark code. + contents = contents.split("# ----------- TESTS START ------------ #")[0] + + if imports_only: + # There is a benchmark test for measuring the time it takes to import the module. + # For that, we exclude from the code everything AFTER this line + contents = contents.split("# ----------- IMPORTS END ------------ #")[0] + else: + # Set the context mode by replacing variable in the script + contents = re.sub(r"CONTEXT_MODE.*?\n", f"CONTEXT_MODE = '{context_mode}'\n", contents, count=1) + + return contents + + +def _get_templating_module( + renderer: TemplatingRenderer, + size: TemplatingTestSize, + context_mode: DjcContextMode, + imports_only: bool, +) -> ModuleType: + if renderer not in ("django", "django-components"): + raise ValueError(f"Invalid renderer: {renderer}") + + file_path = _get_templating_filepath(renderer, size) + script = _get_templating_script(renderer, size, context_mode, imports_only) + + # This makes it possible to import the module in the benchmark function + # as `import test_templating` + module = create_virtual_module("test_templating", script, str(file_path)) + return module + + +# The `timeraw_` tests run in separate processes. But when running memory benchmarks, +# the tested logic runs in the same process as the where we run the benchmark functions +# (e.g. `peakmem_render_lg_first()`). Thus, the `peakmem_` functions have access to this file +# when the tested logic runs. +# +# Secondly, `asv` doesn't offer any way to pass data from `setup` to actual test. +# +# And so we define this global, which, when running memory benchmarks, the `setup` function +# populates. And then we trigger the actual render from within the test body. +do_render = lambda: None # noqa: E731 + + +def setup_templating_memory_benchmark( + renderer: TemplatingRenderer, + size: TemplatingTestSize, + test_type: TemplatingTestType, + context_mode: DjcContextMode, + imports_only: bool = False, +): + global do_render + module = _get_templating_module(renderer, size, context_mode, imports_only) + data = module.gen_render_data() + render = module.render + do_render = lambda: render(data) # noqa: E731 + + # Do the first render as part of setup if we're testing the subsequent renders + if test_type == "subsequent": + do_render() + + +# The timing benchmarks run the actual code in a separate process, by using the `timeraw_` prefix. +# As such, we don't actually load the code in this file. Instead, we only prepare a script (raw string) +# that will be run in the new process. +def prepare_templating_benchmark( + renderer: TemplatingRenderer, + size: TemplatingTestSize, + test_type: TemplatingTestType, + context_mode: DjcContextMode, + imports_only: bool = False, +): + setup_script = _get_templating_script(renderer, size, context_mode, imports_only) + + # If we're testing the startup time, then the setup is actually the tested code + if test_type == "startup": + return setup_script + else: + # Otherwise include also data generation as part of setup + setup_script += "\n\n" "render_data = gen_render_data()\n" + + # Do the first render as part of setup if we're testing the subsequent renders + if test_type == "subsequent": + setup_script += "render(render_data)\n" + + benchmark_script = "render(render_data)\n" + return benchmark_script, setup_script + + +# - Group: django-components vs django +# - time: djc vs django (startup lg) +# - time: djc vs django (lg - FIRST) +# - time: djc vs django (sm - FIRST) +# - time: djc vs django (lg - SUBSEQUENT) +# - time: djc vs django (sm - SUBSEQUENT) +# - mem: djc vs django (lg - FIRST) +# - mem: djc vs django (sm - FIRST) +# - mem: djc vs django (lg - SUBSEQUENT) +# - mem: djc vs django (sm - SUBSEQUENT) +# +# NOTE: While the name suggests we're comparing Django and Django-components, be aware that +# in our "Django" tests, we still install and import django-components. We also use +# django-components's `{% html_attrs %}` tag in the Django scenario. `{% html_attrs %}` +# was used because the original sample code was from django-components. +# +# As such, these tests should seen not as "Using Django vs Using Components". But instead, +# it should be "What is the relative cost of using Components?". +# +# As an example, the benchmarking for the startup time and memory usage is not comparing +# two independent approaches. Rather, the test is checking if defining Components classes +# is more expensive than vanilla Django templates. +class DjangoComponentsVsDjangoTests: + # Testing startup time (e.g. defining classes and templates) + @benchmark( + pretty_name="startup - large", + group_name=DJC_VS_DJ_GROUP, + number=1, + rounds=5, + params={ + "renderer": ["django", "django-components"], + }, + ) + def timeraw_startup_lg(self, renderer: TemplatingRenderer): + return prepare_templating_benchmark(renderer, "lg", "startup", "isolated") + + @benchmark( + pretty_name="render - small - first render", + group_name=DJC_VS_DJ_GROUP, + number=1, + rounds=5, + params={ + "renderer": ["django", "django-components"], + }, + ) + def timeraw_render_sm_first(self, renderer: TemplatingRenderer): + return prepare_templating_benchmark(renderer, "sm", "first", "isolated") + + @benchmark( + pretty_name="render - small - second render", + group_name=DJC_VS_DJ_GROUP, + number=1, + rounds=5, + params={ + "renderer": ["django", "django-components"], + }, + ) + def timeraw_render_sm_subsequent(self, renderer: TemplatingRenderer): + return prepare_templating_benchmark(renderer, "sm", "subsequent", "isolated") + + @benchmark( + pretty_name="render - large - first render", + group_name=DJC_VS_DJ_GROUP, + number=1, + rounds=5, + params={ + "renderer": ["django", "django-components"], + }, + include_in_quick_benchmark=True, + ) + def timeraw_render_lg_first(self, renderer: TemplatingRenderer): + return prepare_templating_benchmark(renderer, "lg", "first", "isolated") + + @benchmark( + pretty_name="render - large - second render", + group_name=DJC_VS_DJ_GROUP, + number=1, + rounds=5, + params={ + "renderer": ["django", "django-components"], + }, + ) + def timeraw_render_lg_subsequent(self, renderer: TemplatingRenderer): + return prepare_templating_benchmark(renderer, "lg", "subsequent", "isolated") + + @benchmark( + pretty_name="render - small - first render (mem)", + group_name=DJC_VS_DJ_GROUP, + number=1, + rounds=5, + params={ + "renderer": ["django", "django-components"], + }, + setup=lambda renderer: setup_templating_memory_benchmark(renderer, "sm", "first", "isolated"), + ) + def peakmem_render_sm_first(self, renderer: TemplatingRenderer): + do_render() + + @benchmark( + pretty_name="render - small - second render (mem)", + group_name=DJC_VS_DJ_GROUP, + number=1, + rounds=5, + params={ + "renderer": ["django", "django-components"], + }, + setup=lambda renderer: setup_templating_memory_benchmark(renderer, "sm", "subsequent", "isolated"), + ) + def peakmem_render_sm_subsequent(self, renderer: TemplatingRenderer): + do_render() + + @benchmark( + pretty_name="render - large - first render (mem)", + group_name=DJC_VS_DJ_GROUP, + number=1, + rounds=5, + params={ + "renderer": ["django", "django-components"], + }, + setup=lambda renderer: setup_templating_memory_benchmark(renderer, "lg", "first", "isolated"), + ) + def peakmem_render_lg_first(self, renderer: TemplatingRenderer): + do_render() + + @benchmark( + pretty_name="render - large - second render (mem)", + group_name=DJC_VS_DJ_GROUP, + number=1, + rounds=5, + params={ + "renderer": ["django", "django-components"], + }, + setup=lambda renderer: setup_templating_memory_benchmark(renderer, "lg", "subsequent", "isolated"), + ) + def peakmem_render_lg_subsequent(self, renderer: TemplatingRenderer): + do_render() + + +# - Group: Django-components "isolated" vs "django" modes +# - time: Isolated vs django djc (startup lg) +# - time: Isolated vs django djc (lg - FIRST) +# - time: Isolated vs django djc (sm - FIRST) +# - time: Isolated vs django djc (lg - SUBSEQUENT) +# - time: Isolated vs django djc (sm - SUBSEQUENT) +# - mem: Isolated vs django djc (lg - FIRST) +# - mem: Isolated vs django djc (sm - FIRST) +# - mem: Isolated vs django djc (lg - SUBSEQUENT) +# - mem: Isolated vs django djc (sm - SUBSEQUENT) +class IsolatedVsDjangoContextModesTests: + # Testing startup time (e.g. defining classes and templates) + @benchmark( + pretty_name="startup - large", + group_name=DJC_ISOLATED_VS_NON_GROUP, + number=1, + rounds=5, + params={ + "context_mode": ["isolated", "django"], + }, + ) + def timeraw_startup_lg(self, context_mode: DjcContextMode): + return prepare_templating_benchmark("django-components", "lg", "startup", context_mode) + + @benchmark( + pretty_name="render - small - first render", + group_name=DJC_ISOLATED_VS_NON_GROUP, + number=1, + rounds=5, + params={ + "context_mode": ["isolated", "django"], + }, + ) + def timeraw_render_sm_first(self, context_mode: DjcContextMode): + return prepare_templating_benchmark("django-components", "sm", "first", context_mode) + + @benchmark( + pretty_name="render - small - second render", + group_name=DJC_ISOLATED_VS_NON_GROUP, + number=1, + rounds=5, + params={ + "context_mode": ["isolated", "django"], + }, + ) + def timeraw_render_sm_subsequent(self, context_mode: DjcContextMode): + return prepare_templating_benchmark("django-components", "sm", "subsequent", context_mode) + + @benchmark( + pretty_name="render - large - first render", + group_name=DJC_ISOLATED_VS_NON_GROUP, + number=1, + rounds=5, + params={ + "context_mode": ["isolated", "django"], + }, + ) + def timeraw_render_lg_first(self, context_mode: DjcContextMode): + return prepare_templating_benchmark("django-components", "lg", "first", context_mode) + + @benchmark( + pretty_name="render - large - second render", + group_name=DJC_ISOLATED_VS_NON_GROUP, + number=1, + rounds=5, + params={ + "context_mode": ["isolated", "django"], + }, + ) + def timeraw_render_lg_subsequent(self, context_mode: DjcContextMode): + return prepare_templating_benchmark("django-components", "lg", "subsequent", context_mode) + + @benchmark( + pretty_name="render - small - first render (mem)", + group_name=DJC_ISOLATED_VS_NON_GROUP, + number=1, + rounds=5, + params={ + "context_mode": ["isolated", "django"], + }, + setup=lambda context_mode: setup_templating_memory_benchmark("django-components", "sm", "first", context_mode), + ) + def peakmem_render_sm_first(self, context_mode: DjcContextMode): + do_render() + + @benchmark( + pretty_name="render - small - second render (mem)", + group_name=DJC_ISOLATED_VS_NON_GROUP, + number=1, + rounds=5, + params={ + "context_mode": ["isolated", "django"], + }, + setup=lambda context_mode: setup_templating_memory_benchmark( + "django-components", + "sm", + "subsequent", + context_mode, + ), + ) + def peakmem_render_sm_subsequent(self, context_mode: DjcContextMode): + do_render() + + @benchmark( + pretty_name="render - large - first render (mem)", + group_name=DJC_ISOLATED_VS_NON_GROUP, + number=1, + rounds=5, + params={ + "context_mode": ["isolated", "django"], + }, + setup=lambda context_mode: setup_templating_memory_benchmark( + "django-components", + "lg", + "first", + context_mode, + ), + ) + def peakmem_render_lg_first(self, context_mode: DjcContextMode): + do_render() + + @benchmark( + pretty_name="render - large - second render (mem)", + group_name=DJC_ISOLATED_VS_NON_GROUP, + number=1, + rounds=5, + params={ + "context_mode": ["isolated", "django"], + }, + setup=lambda context_mode: setup_templating_memory_benchmark( + "django-components", + "lg", + "subsequent", + context_mode, + ), + ) + def peakmem_render_lg_subsequent(self, context_mode: DjcContextMode): + do_render() + + +class OtherTests: + @benchmark( + pretty_name="import time", + group_name=OTHER_GROUP, + number=1, + rounds=5, + ) + def timeraw_import_time(self): + return prepare_templating_benchmark("django-components", "lg", "startup", "isolated", imports_only=True) diff --git a/benchmarks/component_rendering.py b/benchmarks/component_rendering.py deleted file mode 100644 index a067e0e2..00000000 --- a/benchmarks/component_rendering.py +++ /dev/null @@ -1,176 +0,0 @@ -from time import perf_counter - -from django.template import Context, Template - -from django_components import Component, registry, types -from django_components.dependencies import CSS_DEPENDENCY_PLACEHOLDER, JS_DEPENDENCY_PLACEHOLDER -from tests.django_test_setup import * # NOQA -from tests.testutils import BaseTestCase, create_and_process_template_response - - -class SlottedComponent(Component): - template: types.django_html = """ - {% load component_tags %} - -
{% slot "header" %}Default header{% endslot %}
-
{% slot "main" %}Default main{% endslot %}
-
{% slot "footer" %}Default footer{% endslot %}
-
- """ - - -class SimpleComponent(Component): - template: types.django_html = """ - Variable: {{ variable }} - """ - - def get_context_data(self, variable, variable2="default"): - return { - "variable": variable, - "variable2": variable2, - } - - class Media: - css = {"all": ["style.css"]} - js = ["script.js"] - - -class BreadcrumbComponent(Component): - template: types.django_html = """ - - """ - - LINKS = [ - ( - "https://developer.mozilla.org/en-US/docs/Learn", - "Learn web development", - ), - ( - "https://developer.mozilla.org/en-US/docs/Learn/HTML", - "Structuring the web with HTML", - ), - ( - "https://developer.mozilla.org/en-US/docs/Learn/HTML/Introduction_to_HTML", - "Introduction to HTML", - ), - ( - "https://developer.mozilla.org/en-US/docs/Learn/HTML/Introduction_to_HTML/Document_and_website_structure", - "Document and website structure", - ), - ] - - def get_context_data(self, items): - if items > 4: - items = 4 - elif items < 0: - items = 0 - return {"links": self.LINKS[: items - 1]} - - class Media: - css = {"all": ["test.css"]} - js = ["test.js"] - - -EXPECTED_CSS = """""" -EXPECTED_JS = """""" - - -class RenderBenchmarks(BaseTestCase): - def setUp(self): - registry.clear() - registry.register("test_component", SlottedComponent) - registry.register("inner_component", SimpleComponent) - registry.register("breadcrumb_component", BreadcrumbComponent) - - @staticmethod - def timed_loop(func, iterations=1000): - """Run func iterations times, and return the time in ms per iteration.""" - start_time = perf_counter() - for _ in range(iterations): - func() - end_time = perf_counter() - total_elapsed = end_time - start_time # NOQA - return total_elapsed * 1000 / iterations - - def test_render_time_for_small_component(self): - template_str: types.django_html = """ - {% load component_tags %} - {% component 'test_component' %} - {% slot "header" %} - {% component 'inner_component' variable='foo' %}{% endcomponent %} - {% endslot %} - {% endcomponent %} - """ - template = Template(template_str) - - print(f"{self.timed_loop(lambda: template.render(Context({})))} ms per iteration") - - def test_middleware_time_with_dependency_for_small_page(self): - template_str: types.django_html = """ - {% load component_tags %} - {% component_js_dependencies %} - {% component_css_dependencies %} - {% component 'test_component' %} - {% slot "header" %} - {% component 'inner_component' variable='foo' %}{% endcomponent %} - {% endslot %} - {% endcomponent %} - """ - template = Template(template_str) - # Sanity tests - response_content = create_and_process_template_response(template) - self.assertNotIn(CSS_DEPENDENCY_PLACEHOLDER, response_content) - self.assertNotIn(JS_DEPENDENCY_PLACEHOLDER, response_content) - self.assertIn("style.css", response_content) - self.assertIn("script.js", response_content) - - without_middleware = self.timed_loop( - lambda: create_and_process_template_response(template, use_middleware=False) - ) - with_middleware = self.timed_loop(lambda: create_and_process_template_response(template, use_middleware=True)) - - print("Small page middleware test") - self.report_results(with_middleware, without_middleware) - - def test_render_time_with_dependency_for_large_page(self): - from django.template.loader import get_template - - template = get_template("mdn_complete_page.html") - response_content = create_and_process_template_response(template, {}) - self.assertNotIn(CSS_DEPENDENCY_PLACEHOLDER, response_content) - self.assertNotIn(JS_DEPENDENCY_PLACEHOLDER, response_content) - self.assertIn("test.css", response_content) - self.assertIn("test.js", response_content) - - without_middleware = self.timed_loop( - lambda: create_and_process_template_response(template, {}, use_middleware=False) - ) - with_middleware = self.timed_loop( - lambda: create_and_process_template_response(template, {}, use_middleware=True) - ) - - print("Large page middleware test") - self.report_results(with_middleware, without_middleware) - - @staticmethod - def report_results(with_middleware, without_middleware): - print(f"Middleware active\t\t{with_middleware:.3f} ms per iteration") - print(f"Middleware inactive\t{without_middleware:.3f} ms per iteration") - time_difference = with_middleware - without_middleware - if without_middleware > with_middleware: - print(f"Decrease of {-100 * time_difference / with_middleware:.2f}%") - else: - print(f"Increase of {100 * time_difference / without_middleware:.2f}%") diff --git a/benchmarks/monkeypatch_asv.py b/benchmarks/monkeypatch_asv.py new file mode 100644 index 00000000..23003311 --- /dev/null +++ b/benchmarks/monkeypatch_asv.py @@ -0,0 +1,29 @@ +from asv_runner.benchmarks.timeraw import TimerawBenchmark, _SeparateProcessTimer + + +# Fix for https://github.com/airspeed-velocity/asv_runner/pull/44 +def _get_timer(self, *param): + """ + Returns a timer that runs the benchmark function in a separate process. + + #### Parameters + **param** (`tuple`) + : The parameters to pass to the benchmark function. + + #### Returns + **timer** (`_SeparateProcessTimer`) + : A timer that runs the function in a separate process. + """ + if param: + + def func(): + # ---------- OUR CHANGES: ADDED RETURN STATEMENT ---------- + return self.func(*param) + # ---------- OUR CHANGES END ---------- + + else: + func = self.func + return _SeparateProcessTimer(func) + + +TimerawBenchmark._get_timer = _get_timer diff --git a/benchmarks/monkeypatch_asv_ci.txt b/benchmarks/monkeypatch_asv_ci.txt new file mode 100644 index 00000000..30158b6d --- /dev/null +++ b/benchmarks/monkeypatch_asv_ci.txt @@ -0,0 +1,66 @@ +# ------------ FIX FOR #45 ------------ +# See https://github.com/airspeed-velocity/asv_runner/issues/45 +# This fix is applied in CI in the `benchmark.yml` file. +# This file is intentionally named `monkeypatch_asv_ci.txt` to avoid being +# loaded as a python file by `asv`. +# ------------------------------------- + +def timeit(self, number): + """ + Run the function's code `number` times in a separate Python process, and + return the execution time. + + #### Parameters + **number** (`int`) + : The number of times to execute the function's code. + + #### Returns + **time** (`float`) + : The time it took to execute the function's code `number` times. + + #### Notes + The function's code is executed in a separate Python process to avoid + interference from the parent process. The function can return either a + single string of code to be executed, or a tuple of two strings: the + code to be executed and the setup code to be run before timing. + """ + stmt = self.func() + if isinstance(stmt, tuple): + stmt, setup = stmt + else: + setup = "" + stmt = textwrap.dedent(stmt) + setup = textwrap.dedent(setup) + stmt = stmt.replace(r'"""', r"\"\"\"") + setup = setup.replace(r'"""', r"\"\"\"") + + # TODO + # -----------ORIGINAL CODE----------- + # code = self.subprocess_tmpl.format(stmt=stmt, setup=setup, number=number) + + # res = subprocess.check_output([sys.executable, "-c", code]) + # return float(res.strip()) + + # -----------NEW CODE----------- + code = self.subprocess_tmpl.format(stmt=stmt, setup=setup, number=number) + + evaler = textwrap.dedent( + """ + import sys + code = sys.stdin.read() + exec(code) + """ + ) + + proc = subprocess.Popen([sys.executable, "-c", evaler], + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE) + stdout, stderr = proc.communicate(input=code.encode("utf-8")) + if proc.returncode != 0: + raise RuntimeError(f"Subprocess failed: {stderr.decode()}") + return float(stdout.decode("utf-8").strip()) + +_SeparateProcessTimer.timeit = timeit + +# ------------ END FIX #45 ------------ diff --git a/benchmarks/utils.py b/benchmarks/utils.py new file mode 100644 index 00000000..eb160cb0 --- /dev/null +++ b/benchmarks/utils.py @@ -0,0 +1,99 @@ +import os +import sys +from importlib.abc import Loader +from importlib.util import spec_from_loader, module_from_spec +from types import ModuleType +from typing import Any, Dict, List, Optional + + +# NOTE: benchmark_name constraints: +# - MUST BE UNIQUE +# - MUST NOT CONTAIN `-` +# - MUST START WITH `time_`, `mem_`, `peakmem_` +# See https://github.com/airspeed-velocity/asv/pull/1470 +def benchmark( + *, + pretty_name: Optional[str] = None, + timeout: Optional[int] = None, + group_name: Optional[str] = None, + params: Optional[Dict[str, List[Any]]] = None, + number: Optional[int] = None, + min_run_count: Optional[int] = None, + include_in_quick_benchmark: bool = False, + **kwargs, +): + def decorator(func): + # For pull requests, we want to run benchmarks only for a subset of tests, + # because the full set of tests takes about 10 minutes to run (5 min per commit). + # This is done by setting DJC_BENCHMARK_QUICK=1 in the environment. + if os.getenv("DJC_BENCHMARK_QUICK") and not include_in_quick_benchmark: + # By setting the benchmark name to something that does NOT start with + # valid prefixes like `time_`, `mem_`, or `peakmem_`, this function will be ignored by asv. + func.benchmark_name = "noop" + return func + + # "group_name" is our custom field, which we actually convert to asv's "benchmark_name" + if group_name is not None: + benchmark_name = f"{group_name}.{func.__name__}" + func.benchmark_name = benchmark_name + + # Also "params" is custom, so we normalize it to "params" and "param_names" + if params is not None: + func.params, func.param_names = list(params.values()), list(params.keys()) + + if pretty_name is not None: + func.pretty_name = pretty_name + if timeout is not None: + func.timeout = timeout + if number is not None: + func.number = number + if min_run_count is not None: + func.min_run_count = min_run_count + + # Additional, untyped kwargs + for k, v in kwargs.items(): + setattr(func, k, v) + + return func + + return decorator + + +class VirtualModuleLoader(Loader): + def __init__(self, code_string): + self.code_string = code_string + + def exec_module(self, module): + exec(self.code_string, module.__dict__) + + +def create_virtual_module(name: str, code_string: str, file_path: str) -> ModuleType: + """ + To avoid the headaches of importing the tested code from another diretory, + we create a "virtual" module that we can import from anywhere. + + E.g. + ```py + from benchmarks.utils import create_virtual_module + + create_virtual_module("my_module", "print('Hello, world!')", __file__) + + # Now you can import my_module from anywhere + import my_module + ``` + """ + # Create the module specification + spec = spec_from_loader(name, VirtualModuleLoader(code_string)) + + # Create the module + module = module_from_spec(spec) # type: ignore[arg-type] + module.__file__ = file_path + module.__name__ = name + + # Add it to sys.modules + sys.modules[name] = module + + # Execute the module + spec.loader.exec_module(module) # type: ignore[union-attr] + + return module diff --git a/docs/.nav.yml b/docs/.nav.yml new file mode 100644 index 00000000..3816ed34 --- /dev/null +++ b/docs/.nav.yml @@ -0,0 +1,10 @@ +# For navigation content inspo see Pydantic https://docs.pydantic.dev/latest +# +# `.nav.yml` is provided by https://lukasgeiter.github.io/mkdocs-awesome-nav +nav: + - overview + - Getting Started: getting_started + - concepts + - guides + - API Documentation: reference + - Release Notes: release_notes.md diff --git a/docs/SUMMARY.md b/docs/SUMMARY.md deleted file mode 100644 index 6d871c4a..00000000 --- a/docs/SUMMARY.md +++ /dev/null @@ -1,11 +0,0 @@ - -- [Get Started](overview/) -- Concepts - - [Getting started](concepts/getting_started/) - - [Fundamentals](concepts/fundamentals/) - - [Advanced](concepts/advanced/) -- Guides - - [Setup](guides/setup/) - - [Dev guides](guides/devguides/) -- [API Documentation](reference/) -- [Release notes](release_notes.md) diff --git a/docs/benchmarks/asv.css b/docs/benchmarks/asv.css new file mode 100644 index 00000000..d7867516 --- /dev/null +++ b/docs/benchmarks/asv.css @@ -0,0 +1,161 @@ +/* Basic navigation */ + +.asv-navigation { + padding: 2px; +} + +nav ul li.active a { + height: 52px; +} + +nav li.active span.navbar-brand { + background-color: #e7e7e7; + height: 52px; +} + +nav li.active span.navbar-brand:hover { + background-color: #e7e7e7; +} + +.navbar-default .navbar-link { + color: #2458D9; +} + +.panel-body { + padding: 0; +} + +.panel { + margin-bottom: 4px; + -webkit-box-shadow: none; + box-shadow: none; + border-radius: 0; + border-top-left-radius: 3px; + border-top-right-radius: 3px; +} + +.panel-default>.panel-heading, +.panel-heading { + font-size: 12px; + font-weight:bold; + padding: 2px; + text-align: center; + border-top-left-radius: 3px; + border-top-right-radius: 3px; + background-color: #eee; +} + +.btn, +.btn-group, +.btn-group-vertical>.btn:first-child, +.btn-group-vertical>.btn:last-child:not(:first-child), +.btn-group-vertical>.btn:last-child { + border: none; + border-radius: 0px; + overflow: hidden; +} + +.btn-default:focus, .btn-default:active, .btn-default.active { + border: none; + color: #fff; + background-color: #99bfcd; +} + +#range { + font-family: monospace; + text-align: center; + background: #ffffff; +} + +.form-control { + border: none; + border-radius: 0px; + font-size: 12px; + padding: 0px; +} + +.tooltip-inner { + min-width: 100px; + max-width: 800px; + text-align: left; + white-space: pre-wrap; + font-family: monospace; +} + +/* Benchmark tree */ + +.nav-list { + font-size: 12px; + padding: 0; + padding-left: 15px; +} + +.nav-list>li { + overflow-x: hidden; +} + +.nav-list>li>a { + padding: 0; + padding-left: 5px; + color: #000; +} + +.nav-list>li>a:focus { + color: #fff; + background-color: #99bfcd; + box-shadow: inset 0 3px 5px rgba(0,0,0,.125); +} + +.nav-list>li>.nav-header { + white-space: nowrap; + font-weight: 500; + margin-bottom: 2px; +} + +.caret-right { + display: inline-block; + width: 0; + height: 0; + margin-left: 2px; + vertical-align: middle; + border-left: 4px solid; + border-bottom: 4px solid transparent; + border-top: 4px solid transparent; +} + +/* Summary page */ + +.benchmark-group > h1 { + text-align: center; +} + +.benchmark-container { + width: 300px; + height: 116px; + padding: 4px; + border-radius: 3px; +} + +.benchmark-container:hover { + background-color: #eee; +} + +.benchmark-plot { + width: 292px; + height: 88px; +} + +.benchmark-text { + font-size: 12px; + color: #000; + width: 292px; + overflow: hidden; +} + +#extra-buttons { + margin: 1em; +} + +#extra-buttons a { + border: solid 1px #ccc; +} diff --git a/docs/benchmarks/asv.js b/docs/benchmarks/asv.js new file mode 100644 index 00000000..ac235639 --- /dev/null +++ b/docs/benchmarks/asv.js @@ -0,0 +1,525 @@ +'use strict'; + +$(document).ready(function() { + /* GLOBAL STATE */ + /* The index.json content as returned from the server */ + var main_timestamp = ''; + var main_json = {}; + /* Extra pages: {name: show_function} */ + var loaded_pages = {}; + /* Previous window scroll positions */ + var window_scroll_positions = {}; + /* Previous window hash location */ + var window_last_location = null; + /* Graph data cache */ + var graph_cache = {}; + var graph_cache_max_size = 5; + + var colors = [ + '#247AAD', + '#E24A33', + '#988ED5', + '#777777', + '#FBC15E', + '#8EBA42', + '#FFB5B8' + ]; + + var time_units = [ + ['ps', 'picoseconds', 0.000000000001], + ['ns', 'nanoseconds', 0.000000001], + ['μs', 'microseconds', 0.000001], + ['ms', 'milliseconds', 0.001], + ['s', 'seconds', 1], + ['m', 'minutes', 60], + ['h', 'hours', 60 * 60], + ['d', 'days', 60 * 60 * 24], + ['w', 'weeks', 60 * 60 * 24 * 7], + ['y', 'years', 60 * 60 * 24 * 7 * 52], + ['C', 'centuries', 60 * 60 * 24 * 7 * 52 * 100] + ]; + + var mem_units = [ + ['', 'bytes', 1], + ['k', 'kilobytes', 1000], + ['M', 'megabytes', 1000000], + ['G', 'gigabytes', 1000000000], + ['T', 'terabytes', 1000000000000] + ]; + + function pretty_second(x) { + for (var i = 0; i < time_units.length - 1; ++i) { + if (Math.abs(x) < time_units[i+1][2]) { + return (x / time_units[i][2]).toFixed(3) + time_units[i][0]; + } + } + + return 'inf'; + } + + function pretty_byte(x) { + for (var i = 0; i < mem_units.length - 1; ++i) { + if (Math.abs(x) < mem_units[i+1][2]) { + break; + } + } + if (i == 0) { + return x + ''; + } + return (x / mem_units[i][2]).toFixed(3) + mem_units[i][0]; + } + + function pretty_unit(x, unit) { + if (unit == "seconds") { + return pretty_second(x); + } + else if (unit == "bytes") { + return pretty_byte(x); + } + else if (unit && unit != "unit") { + return '' + x.toPrecision(3) + ' ' + unit; + } + else { + return '' + x.toPrecision(3); + } + } + + function pad_left(s, c, num) { + s = '' + s; + while (s.length < num) { + s = c + s; + } + return s; + } + + function format_date_yyyymmdd(date) { + return (pad_left(date.getFullYear(), '0', 4) + + '-' + pad_left(date.getMonth() + 1, '0', 2) + + '-' + pad_left(date.getDate(), '0', 2)); + } + + function format_date_yyyymmdd_hhmm(date) { + return (format_date_yyyymmdd(date) + ' ' + + pad_left(date.getHours(), '0', 2) + + ':' + pad_left(date.getMinutes(), '0', 2)); + } + + /* Convert a flat index to permutation to the corresponding value */ + function param_selection_from_flat_idx(params, idx) { + var selection = []; + if (idx < 0) { + idx = 0; + } + for (var k = params.length-1; k >= 0; --k) { + var j = idx % params[k].length; + selection.unshift([j]); + idx = (idx - j) / params[k].length; + } + selection.unshift([null]); + return selection; + } + + /* Convert a benchmark parameter value from their native Python + repr format to a number or a string, ready for presentation */ + function convert_benchmark_param_value(value_repr) { + var match = Number(value_repr); + if (!isNaN(match)) { + return match; + } + + /* Python str */ + match = value_repr.match(/^'(.+)'$/); + if (match) { + return match[1]; + } + + /* Python unicode */ + match = value_repr.match(/^u'(.+)'$/); + if (match) { + return match[1]; + } + + /* Python class */ + match = value_repr.match(/^$/); + if (match) { + return match[1]; + } + + return value_repr; + } + + /* Convert loaded graph data to a format flot understands, by + treating either time or one of the parameters as x-axis, + and selecting only one value of the remaining axes */ + function filter_graph_data(raw_series, x_axis, other_indices, params) { + if (params.length == 0) { + /* Simple time series */ + return raw_series; + } + + /* Compute position of data entry in the results list, + and stride corresponding to plot x-axis parameter */ + var stride = 1; + var param_stride = 0; + var param_idx = 0; + for (var k = params.length - 1; k >= 0; --k) { + if (k == x_axis - 1) { + param_stride = stride; + } + else { + param_idx += other_indices[k + 1] * stride; + } + stride *= params[k].length; + } + + if (x_axis == 0) { + /* x-axis is time axis */ + var series = new Array(raw_series.length); + for (var k = 0; k < raw_series.length; ++k) { + if (raw_series[k][1] === null) { + series[k] = [raw_series[k][0], null]; + } else { + series[k] = [raw_series[k][0], + raw_series[k][1][param_idx]]; + } + } + return series; + } + else { + /* x-axis is some parameter axis */ + var time_idx = null; + if (other_indices[0] === null) { + time_idx = raw_series.length - 1; + } + else { + /* Need to search for the correct time value */ + for (var k = 0; k < raw_series.length; ++k) { + if (raw_series[k][0] == other_indices[0]) { + time_idx = k; + break; + } + } + if (time_idx === null) { + /* No data points */ + return []; + } + } + + var x_values = params[x_axis - 1]; + var series = new Array(x_values.length); + for (var k = 0; k < x_values.length; ++k) { + if (raw_series[time_idx][1] === null) { + series[k] = [convert_benchmark_param_value(x_values[k]), + null]; + } + else { + series[k] = [convert_benchmark_param_value(x_values[k]), + raw_series[time_idx][1][param_idx]]; + } + param_idx += param_stride; + } + return series; + } + } + + function filter_graph_data_idx(raw_series, x_axis, flat_idx, params) { + var selection = param_selection_from_flat_idx(params, flat_idx); + var flat_selection = []; + $.each(selection, function(i, v) { + flat_selection.push(v[0]); + }); + return filter_graph_data(raw_series, x_axis, flat_selection, params); + } + + /* Escape special characters in graph item file names. + The implementation must match asv.util.sanitize_filename */ + function sanitize_filename(name) { + var bad_re = /[<>:"\/\\^|?*\x00-\x1f]/g; + var bad_names = ["CON", "PRN", "AUX", "NUL", "COM1", "COM2", "COM3", + "COM4", "COM5", "COM6", "COM7", "COM8", "COM9", "LPT1", + "LPT2", "LPT3", "LPT4", "LPT5", "LPT6", "LPT7", "LPT8", + "LPT9"]; + name = name.replace(bad_re, "_"); + if (bad_names.indexOf(name.toUpperCase()) != -1) { + name = name + "_"; + } + return name; + } + + /* Given a specific group of parameters, generate the URL to + use to load that graph. + The implementation must match asv.graph.Graph.get_file_path + */ + function graph_to_path(benchmark_name, state) { + var parts = []; + $.each(state, function(key, value) { + var part; + if (value === null) { + part = key + "-null"; + } else if (value) { + part = key + "-" + value; + } else { + part = key; + } + parts.push(sanitize_filename('' + part)); + }); + parts.sort(); + parts.splice(0, 0, "graphs"); + parts.push(sanitize_filename(benchmark_name)); + + /* Escape URI components */ + parts = $.map(parts, function (val) { return encodeURIComponent(val); }); + return parts.join('/') + ".json"; + } + + /* + Load and cache graph data (on javascript side) + */ + function load_graph_data(url, success, failure) { + var dfd = $.Deferred(); + if (graph_cache[url]) { + setTimeout(function() { + dfd.resolve(graph_cache[url]); + }, 1); + } + else { + $.ajax({ + url: url + '?timestamp=' + $.asv.main_timestamp, + dataType: "json", + cache: true + }).done(function(data) { + if (Object.keys(graph_cache).length > graph_cache_max_size) { + $.each(Object.keys(graph_cache), function (i, key) { + delete graph_cache[key]; + }); + } + graph_cache[url] = data; + dfd.resolve(data); + }).fail(function() { + dfd.reject(); + }); + } + return dfd.promise(); + } + + /* + Parse hash string, assuming format similar to standard URL + query strings + */ + function parse_hash_string(str) { + var info = {location: [''], params: {}}; + + if (str && str[0] == '#') { + str = str.slice(1); + } + if (str && str[0] == '/') { + str = str.slice(1); + } + + var match = str.match(/^([^?]*?)\?/); + if (match) { + info['location'] = decodeURIComponent(match[1]).replace(/\/+/, '/').split('/'); + var rest = str.slice(match[1].length+1); + var parts = rest.split('&'); + for (var i = 0; i < parts.length; ++i) { + var part = parts[i].split('='); + if (part.length != 2) { + continue; + } + var key = decodeURIComponent(part[0].replace(/\+/g, " ")); + var value = decodeURIComponent(part[1].replace(/\+/g, " ")); + if (value == '[none]') { + value = null; + } + if (info['params'][key] === undefined) { + info['params'][key] = [value]; + } + else { + info['params'][key].push(value); + } + } + } + else { + info['location'] = decodeURIComponent(str).replace(/\/+/, '/').split('/'); + } + return info; + } + + /* + Generate a hash string, inverse of parse_hash_string + */ + function format_hash_string(info) { + var parts = info['params']; + var str = '#' + info['location']; + + if (parts) { + str = str + '?'; + var first = true; + $.each(parts, function (key, values) { + $.each(values, function (idx, value) { + if (!first) { + str = str + '&'; + } + if (value === null) { + value = '[none]'; + } + str = str + encodeURIComponent(key) + '=' + encodeURIComponent(value); + first = false; + }); + }); + } + return str; + } + + /* + Dealing with sub-pages + */ + + function show_page(name, params) { + if (loaded_pages[name] !== undefined) { + $("#nav ul li.active").removeClass('active'); + $("#nav-li-" + name).addClass('active'); + $("#graph-display").hide(); + $("#summarygrid-display").hide(); + $("#summarylist-display").hide(); + $('#regressions-display').hide(); + $('.tooltip').remove(); + loaded_pages[name](params); + return true; + } + else { + return false; + } + } + + function hashchange() { + var info = parse_hash_string(window.location.hash); + + /* Keep track of window scroll position; makes the back-button work */ + var old_scroll_pos = window_scroll_positions[info.location.join('/')]; + window_scroll_positions[window_last_location] = $(window).scrollTop(); + window_last_location = info.location.join('/'); + + /* Redirect to correct handler */ + if (show_page(info.location, info.params)) { + /* show_page does the work */ + } + else { + /* Display benchmark page */ + info.params['benchmark'] = info.location[0]; + show_page('graphdisplay', info.params); + } + + /* Scroll back to previous position, if any */ + if (old_scroll_pos !== undefined) { + $(window).scrollTop(old_scroll_pos); + } + } + + function get_commit_hash(revision) { + var commit_hash = main_json.revision_to_hash[revision]; + if (commit_hash) { + // Return printable commit hash + commit_hash = commit_hash.slice(0, main_json.hash_length); + } + return commit_hash; + } + + function get_revision(commit_hash) { + var rev = null; + $.each(main_json.revision_to_hash, function(revision, full_commit_hash) { + if (full_commit_hash.startsWith(commit_hash)) { + rev = revision; + // break the $.each loop + return false; + } + }); + return rev; + } + + function init_index() { + /* Fetch the main index.json and then set up the page elements + based on it. */ + $.ajax({ + url: "index.json" + '?timestamp=' + $.asv.main_timestamp, + dataType: "json", + cache: true + }).done(function (index) { + main_json = index; + $.asv.main_json = index; + + /* Page title */ + var project_name = $("#project-name")[0]; + project_name.textContent = index.project; + project_name.setAttribute("href", index.project_url); + $("#project-name").textContent = index.project; + document.title = "airspeed velocity of an unladen " + index.project; + + $(window).on('hashchange', hashchange); + + $('#graph-display').hide(); + $('#regressions-display').hide(); + $('#summarygrid-display').hide(); + $('#summarylist-display').hide(); + + hashchange(); + }).fail(function () { + $.asv.ui.network_error(); + }); + } + + function init() { + /* Fetch the info.json */ + $.ajax({ + url: "info.json", + dataType: "json", + cache: false + }).done(function (info) { + main_timestamp = info['timestamp']; + $.asv.main_timestamp = main_timestamp; + init_index(); + }).fail(function () { + $.asv.ui.network_error(); + }); + } + + + /* + Set up $.asv + */ + + this.register_page = function(name, show_function) { + loaded_pages[name] = show_function; + } + this.parse_hash_string = parse_hash_string; + this.format_hash_string = format_hash_string; + + this.filter_graph_data = filter_graph_data; + this.filter_graph_data_idx = filter_graph_data_idx; + this.convert_benchmark_param_value = convert_benchmark_param_value; + this.param_selection_from_flat_idx = param_selection_from_flat_idx; + this.graph_to_path = graph_to_path; + this.load_graph_data = load_graph_data; + this.get_commit_hash = get_commit_hash; + this.get_revision = get_revision; + + this.main_timestamp = main_timestamp; /* Updated after info.json loads */ + this.main_json = main_json; /* Updated after index.json loads */ + + this.format_date_yyyymmdd = format_date_yyyymmdd; + this.format_date_yyyymmdd_hhmm = format_date_yyyymmdd_hhmm; + this.pretty_unit = pretty_unit; + this.time_units = time_units; + this.mem_units = mem_units; + + this.colors = colors; + + $.asv = this; + + + /* + Launch it + */ + + init(); +}); diff --git a/docs/benchmarks/asv_ui.js b/docs/benchmarks/asv_ui.js new file mode 100644 index 00000000..af757c70 --- /dev/null +++ b/docs/benchmarks/asv_ui.js @@ -0,0 +1,231 @@ +'use strict'; + +$(document).ready(function() { + function make_panel(nav, heading) { + var panel = $('
'); + nav.append(panel); + var panel_header = $( + '
' + heading + '
'); + panel.append(panel_header); + var panel_body = $('
'); + panel.append(panel_body); + return panel_body; + } + + function make_value_selector_panel(nav, heading, values, setup_callback) { + var panel_body = make_panel(nav, heading); + var vertical = false; + var buttons = $('
'); + + panel_body.append(buttons); + + $.each(values, function (idx, value) { + var button = $( + ''); + setup_callback(idx, value, button); + buttons.append(button); + }); + + return panel_body; + } + + function reflow_value_selector_panels(no_timeout) { + $('.panel').each(function (i, panel_obj) { + var panel = $(panel_obj); + panel.find('.btn-group').each(function (i, buttons_obj) { + var buttons = $(buttons_obj); + var width = 0; + + if (buttons.hasClass('reflow-done')) { + /* already processed */ + return; + } + + $.each(buttons.children(), function(idx, value) { + width += value.scrollWidth; + }); + + var max_width = panel_obj.clientWidth; + + if (width >= max_width) { + buttons.addClass("btn-group-vertical"); + buttons.css("width", "100%"); + buttons.css("max-height", "20ex"); + buttons.css("overflow-y", "auto"); + } + else { + buttons.addClass("btn-group-justified"); + } + + /* The widths can be zero if the UI is not fully layouted yet, + so mark the adjustment complete only if this is not the case */ + if (width > 0 && max_width > 0) { + buttons.addClass("reflow-done"); + } + }); + }); + + if (!no_timeout) { + /* Call again asynchronously, in case the UI was not fully layouted yet */ + setTimeout(function() { $.asv.ui.reflow_value_selector_panels(true); }, 0); + } + } + + function network_error(ajax, status, error) { + $("#error-message").text( + "Error fetching content. " + + "Perhaps web server has gone down."); + $("#error").modal('show'); + } + + function hover_graph(element, graph_url, benchmark_basename, parameter_idx, revisions) { + /* Show the summary graph as a popup */ + var plot_div = $('
'); + plot_div.css('width', '11.8em'); + plot_div.css('height', '7em'); + plot_div.css('border', '2px solid black'); + plot_div.css('background-color', 'white'); + + function update_plot() { + var markings = []; + + if (revisions) { + $.each(revisions, function(i, revs) { + var rev_a = revs[0]; + var rev_b = revs[1]; + + if (rev_a !== null) { + markings.push({ color: '#d00', lineWidth: 2, xaxis: { from: rev_a, to: rev_a }}); + markings.push({ color: "rgba(255,0,0,0.1)", xaxis: { from: rev_a, to: rev_b }}); + } + markings.push({ color: '#d00', lineWidth: 2, xaxis: { from: rev_b, to: rev_b }}); + }); + } + + $.asv.load_graph_data( + graph_url + ).done(function (data) { + var params = $.asv.main_json.benchmarks[benchmark_basename].params; + data = $.asv.filter_graph_data_idx(data, 0, parameter_idx, params); + var options = { + colors: ['#000'], + series: { + lines: { + show: true, + lineWidth: 2 + }, + shadowSize: 0 + }, + grid: { + borderWidth: 1, + margin: 0, + labelMargin: 0, + axisMargin: 0, + minBorderMargin: 0, + markings: markings, + }, + xaxis: { + ticks: [], + }, + yaxis: { + ticks: [], + min: 0 + }, + legend: { + show: false + } + }; + var plot = $.plot(plot_div, [{data: data}], options); + }).fail(function () { + // TODO: Handle failure + }); + + return plot_div; + } + + element.popover({ + placement: 'left auto', + trigger: 'hover', + html: true, + delay: 50, + content: $('
').append(plot_div) + }); + + element.on('show.bs.popover', update_plot); + } + + function hover_summary_graph(element, benchmark_basename) { + /* Show the summary graph as a popup */ + var plot_div = $('
'); + plot_div.css('width', '11.8em'); + plot_div.css('height', '7em'); + plot_div.css('border', '2px solid black'); + plot_div.css('background-color', 'white'); + + function update_plot() { + var markings = []; + + $.asv.load_graph_data( + 'graphs/summary/' + benchmark_basename + '.json' + ).done(function (data) { + var options = { + colors: $.asv.colors, + series: { + lines: { + show: true, + lineWidth: 2 + }, + shadowSize: 0 + }, + grid: { + borderWidth: 1, + margin: 0, + labelMargin: 0, + axisMargin: 0, + minBorderMargin: 0, + markings: markings, + }, + xaxis: { + ticks: [], + }, + yaxis: { + ticks: [], + min: 0 + }, + legend: { + show: false + } + }; + var plot = $.plot(plot_div, [{data: data}], options); + }).fail(function () { + // TODO: Handle failure + }); + + return plot_div; + } + + element.popover({ + placement: 'left auto', + trigger: 'hover', + html: true, + delay: 50, + content: $('
').append(plot_div) + }); + + element.on('show.bs.popover', update_plot); + } + + /* + Set up $.asv.ui + */ + + this.network_error = network_error; + this.make_panel = make_panel; + this.make_value_selector_panel = make_value_selector_panel; + this.reflow_value_selector_panels = reflow_value_selector_panels; + this.hover_graph = hover_graph; + this.hover_summary_graph = hover_summary_graph; + + $.asv.ui = this; +}); diff --git a/docs/benchmarks/error.html b/docs/benchmarks/error.html new file mode 100644 index 00000000..af2a4d54 --- /dev/null +++ b/docs/benchmarks/error.html @@ -0,0 +1,23 @@ + + + + airspeed velocity error + + + + +

+ swallow + Can not determine continental origin of swallow. +

+ +

+ One or more external (JavaScript) dependencies of airspeed velocity failed to load. +

+ +

+ Make sure you have an active internet connection and enable 3rd-party scripts + in your browser the first time you load airspeed velocity. +

+ + diff --git a/docs/benchmarks/graphdisplay.js b/docs/benchmarks/graphdisplay.js new file mode 100644 index 00000000..ba715322 --- /dev/null +++ b/docs/benchmarks/graphdisplay.js @@ -0,0 +1,1427 @@ +'use strict'; + +$(document).ready(function() { + /* The state of the parameters in the sidebar. Dictionary mapping + strings to arrays containing the "enabled" configurations. */ + var state = null; + /* The name of the current benchmark being displayed. */ + var current_benchmark = null; + /* An array of graphs being displayed. */ + var graphs = []; + var orig_graphs = []; + /* An array of commit revisions being displayed */ + var current_revisions = []; + /* True when log scaling is enabled. */ + var log_scale = false; + /* True when zooming in on the y-axis. */ + var zoom_y_axis = false; + /* True when log scaling is enabled. */ + var reference_scale = false; + /* True when selecting a reference point */ + var select_reference = false; + /* The reference value */ + var reference = 1.0; + /* Whether to show the legend */ + var show_legend = true; + /* Is even commit spacing being used? */ + var even_spacing = false; + var even_spacing_revisions = []; + /* Is date scale being used ? */ + var date_scale = false; + var date_to_revision = {}; + /* A little div to handle tooltip placement on the graph */ + var tooltip = null; + /* X-axis coordinate axis in the data set; always 0 for + non-parameterized tests where revision and date are the only potential x-axis */ + var x_coordinate_axis = 0; + var x_coordinate_is_category = false; + /* List of lists of value combinations to plot (apart from x-axis) + in parameterized tests. */ + var benchmark_param_selection = [[null]]; + /* Highlighted revisions */ + var highlighted_revisions = null; + /* Whether benchmark graph display was set up */ + var benchmark_graph_display_ready = false; + + + /* UTILITY FUNCTIONS */ + function arr_remove_from(a, x) { + var out = []; + $.each(a, function(i, val) { + if (x !== val) { + out.push(val); + } + }); + return out; + } + + function obj_copy(obj) { + var newobj = {}; + $.each(obj, function(key, val) { + newobj[key] = val; + }); + return newobj; + } + + function obj_length(obj) { + var i = 0; + for (var x in obj) + ++i; + return i; + } + + function obj_get_first_key(data) { + for (var prop in data) + return prop; + } + + function no_data(ajax, status, error) { + $("#error-message").text( + "No data for this combination of filters. "); + $("#error").modal('show'); + } + + function get_x_from_revision(rev) { + if (date_scale) { + return $.asv.main_json.revision_to_date[rev]; + } else { + return rev; + } + } + + function get_commit_hash(x) { + // Return the commit hash in the current graph located at position x + if (date_scale) { + x = date_to_revision[x]; + } + return $.asv.get_commit_hash(x); + } + + + function display_benchmark(bm_name, state_selection, highlight_revisions) { + setup_benchmark_graph_display(); + + $('#graph-display').show(); + $('#summarygrid-display').hide(); + $('#regressions-display').hide(); + $('.tooltip').remove(); + + if (reference_scale) { + reference_scale = false; + $('#reference').removeClass('active'); + reference = 1.0; + } + current_benchmark = bm_name; + highlighted_revisions = highlight_revisions; + $("#title").text(bm_name); + setup_benchmark_params(state_selection); + replace_graphs(); + } + + function setup_benchmark_graph_display() { + if (benchmark_graph_display_ready) { + return; + } + benchmark_graph_display_ready = true; + + /* When the window resizes, redraw the graphs */ + $(window).on('resize', function() { + update_graphs(); + }); + + var nav = $("#graphdisplay-navigation"); + + /* Make the static tooltips look correct */ + $('[data-toggle="tooltip"]').tooltip({container: 'body'}); + + /* Add insertion point for benchmark parameters */ + var state_params_nav = $("
"); + nav.append(state_params_nav); + + /* Add insertion point for benchmark parameters */ + var bench_params_nav = $("
"); + nav.append(bench_params_nav); + + /* Benchmark panel */ + var panel_body = $.asv.ui.make_panel(nav, 'benchmark'); + + var tree = $('