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 e9c3c697..d13f870d 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -2,15 +2,14 @@ name: Docs - build & deploy on: push: - branches: [master] + tags: + # for versions 0.### (before 1.0.0) + - '0.[0-9]+' + # after 1.0.0 + - '[0-9]+.[0-9]+.[0-9]+' + branches: + - master workflow_dispatch: - inputs: - ref: - description: "The commit SHA, tag, or branch to publish. Uses the default branch if not specified." - default: "" - type: string - release: - types: [published] jobs: docs: @@ -20,53 +19,177 @@ jobs: pages: write # to deploy to Pages id-token: write # to verify the deployment originates from an appropriate source runs-on: ubuntu-latest - if: github.ref == 'refs/heads/master' && github.repository_owner == 'EmilStenstrom' + # Only run in original repo (not in forks) + if: github.repository == 'django-components/django-components' steps: - - uses: actions/checkout@v4 + + ############################## + # 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.12" + python-version: "3.13" + cache: 'pip' - - name: Install Hatch + - name: Install dependencies run: | python -m pip install --upgrade pip wheel - python -m pip install -q hatch pre-commit + # 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 - - name: Create Virtual Environment - run: hatch env create docs + ########################################### + # RECORD BENCHMARK - ONLY ON PUSH TO MASTER + ########################################### - - name: "Check for mkdocs build --strict" - # XXX Enable strict mode once docs are clean + - 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: | - hatch run docs:build - # hatch run docs:build --strict - # If pull request or not master branch and not a tag - if: github.event_name == 'pull_request' || (github.event_name == 'push' && github.ref != 'refs/heads/master' && !startsWith(github.ref, 'refs/tags/')) || github.event_name == 'workflow_dispatch' + # 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: Configure git run: | + # required for "mike deploy" command below which pushes to gh-pages git config user.name github-actions git config user.email github-actions@github.com - - name: Deploy docs (dev) - if: github.event_name == 'push' && github.ref_name == 'master' && github.ref_type == 'branch' - run: | - export SHORT_SHA=$(echo "${GITHUB_SHA}" | cut -c1-7) - hatch run docs:mike deploy dev --update-aliases --title "dev (${SHORT_SHA})" --push --alias-type=redirect + # Conditions make sure to select the right step, depending on the job trigger. + # Only one of the steps below will run at a time. The others will be skipped. - - name: Deploy docs (tag) + - name: Check docs in pull requests with strict mode + if: github.event_name == 'pull_request' + run: | + # XXX Enable strict mode once docs are clean + echo "Strict check of docs disabled." + # hatch run docs:build --strict + + - 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 ${{ github.ref_name }} latest --push --update-aliases --alias-type=redirect - 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: Deploy docs (Released published) - if: github.event_name == 'release' && github.event.action == 'published' && github.ref_type == 'tag' - run: | - # Version from tag, keep leading v, from github.ref workflow variable - hatch run docs:mike deploy ${{ github.ref_name }} latest --push --update-aliases --alias-type=redirect + 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 8fcc25e8..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 == '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 0b751024..bc0ecb99 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -4,18 +4,31 @@ on: push: branches: - 'master' + - 'dev' pull_request: workflow_dispatch: jobs: build: - runs-on: ubuntu-20.04 + runs-on: ${{ matrix.os }} strategy: matrix: - python-version: ['3.8', '3.9', '3.10', '3.11', '3.12'] + python-version: ['3.8', '3.9', '3.10', '3.11', '3.12', '3.13'] + 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 }} uses: actions/setup-python@v5 with: @@ -24,6 +37,65 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip - python -m pip install tox tox-gh-actions + python -m pip install -r requirements-ci.txt + # See https://playwright.dev/python/docs/intro#installing-playwright-pytest + playwright install chromium --with-deps - name: Run tests run: tox + + # Verify that docs build + test_docs: + 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: | + python -m pip install --upgrade pip + python -m pip install -r requirements-docs.txt + # Install your package locally + 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 389be594..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 @@ -50,6 +54,7 @@ coverage.xml # Django stuff: *.log +*.sqlite3 # Sphinx documentation docs/_build/ @@ -73,4 +78,13 @@ poetry.lock .DS_Store .python-version site -docs/reference +.direnv/ +.envrc +.mypy_cache/ + +# JS, NPM Dependency directories +node_modules/ +jspm_packages/ + +# Cursor +.cursorrules \ No newline at end of file diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 31b2e2f2..18541677 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -4,7 +4,7 @@ repos: hooks: - id: isort - repo: https://github.com/psf/black - rev: 24.8.0 + rev: 24.10.0 hooks: - id: black - repo: https://github.com/pycqa/flake8 diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 00000000..9cabbd37 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,2655 @@ +# 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/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://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/django-components/django-components/pull/855). + +## v0.120 + +⚠️ Attention ⚠️ - Please update to v0.121 to fix bugs introduced in v0.119. + +#### Fix + +- 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/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/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/django-components/django-components/pull/825) + +- Internal parsing of template tags tag was updated. No API change. (https://github.com/django-components/django-components/pull/827) + +## v0.118 + +#### Feat + +- Add support for `context_processors` and `RenderContext` inside component templates + + `Component.render()` and `Component.render_to_response()` now accept an extra kwarg `request`. + + ```py + def my_view(request) + return MyTable.render_to_response( + request=request + ) + ``` + + - When you pass in `request`, the component will use `RenderContext` instead of `Context`. + Thus the context processors will be applied to the context. + + - NOTE: When you pass in both `request` and `context` to `Component.render()`, and `context` is already an instance of `Context`, the `request` kwarg will be ignored. + +## v0.117 + +#### Fix + +- The HTML parser no longer erronously inserts `` on some occasions, and + no longer tries to close unclosed HTML tags. + +#### Refactor + +- Replaced [Selectolax](https://github.com/rushter/selectolax) with [BeautifulSoup4](https://www.crummy.com/software/BeautifulSoup/bs4/doc/) as project dependencies. + +## v0.116 + +⚠️ 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 + +- Fix the order of execution of JS scripts: + - Scripts in `Component.Media.js` are executed in the order they are defined + - Scripts in `Component.js` are executed AFTER `Media.js` scripts + +- Fix compatibility with AlpineJS + - Scripts in `Component.Media.js` are now again inserted as ` + + + {% component 'my_alpine_component' / %} + {% component_js_dependencies %} + + + ``` + + Option 2 - AlpineJS loaded in `` AFTER `{% component_js_depenencies %}`: + ```html + + + {% component_css_dependencies %} + + + {% component 'my_alpine_component' / %} + {% component_js_dependencies %} + + + + + ``` + +## v0.115 + +⚠️ 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 + +- Fix integration with ManifestStaticFilesStorage on Windows by resolving component filepaths + (like `Component.template_name`) to POSIX paths. + +## v0.114 + +⚠️ 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 + +- Prevent rendering Slot tags during fill discovery stage to fix a case when a component inside a slot + fill tried to access provided data too early. + +## v0.113 + +⚠️ 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 + +- Ensure consistent order of scripts in `Component.Media.js` + +## v0.112 + +⚠️ 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 + +- Allow components to accept default fill even if no default slot was encountered during rendering + +## v0.111 + +⚠️ 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 + +- Prevent rendering Component tags during fill discovery stage to fix a case when a component inside the default slot + tried to access provided data too early. + +## 🚨📢 v0.110 + +⚠️ 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 + +#### 🚨📢 BREAKING CHANGES + +- 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/django-components/django-components#adding-support-for-js-and-css)") + +- Component typing signature changed from + + ```py + Component[Args, Kwargs, Data, Slots] + ``` + + to + + ```py + Component[Args, Kwargs, Slots, Data, JsData, CssData] + ``` + +- If you rendered a component A with `Component.render()` and then inserted that into another component B, now you must pass `render_dependencies=False` to component A: + + ```py + prerendered_a = CompA.render( + args=[...], + kwargs={...}, + render_dependencies=False, + ) + + html = CompB.render( + kwargs={ + content=prerendered_a, + }, + ) + ``` + +#### Feat + +- Intellisense and mypy validation for settings: + + Instead of defining the `COMPONENTS` settings as a plain dict, you can use `ComponentsSettings`: + + ```py + # settings.py + from django_components import ComponentsSettings + + COMPONENTS = ComponentsSettings( + autodiscover=True, + ... + ) + ``` + +- Use `get_component_dirs()` and `get_component_files()` to get the same list of dirs / files that would be imported by `autodiscover()`, but without actually +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/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: + - `RegistrySettings.CONTEXT_BEHAVIOR` -> `RegistrySettings.context_behavior` + - `RegistrySettings.TAG_FORMATTER` -> `RegistrySettings.tag_formatter` + + The old uppercase settings `CONTEXT_BEHAVIOR` and `TAG_FORMATTER` are deprecated and will be removed in v1. + +- The setting `reload_on_template_change` was renamed to + [`reload_on_file_change`](../settings#django_components.app_settings.ComponentsSettings#reload_on_file_change). + And now it properly triggers server reload when any file in the component dirs change. The old name `reload_on_template_change` + is deprecated and will be removed in v1. + +- The setting `forbidden_static_files` was renamed to + [`static_files_forbidden`](../settings#django_components.app_settings.ComponentsSettings#static_files_forbidden) + to align with [`static_files_allowed`](../settings#django_components.app_settings.ComponentsSettings#static_files_allowed) + The old name `forbidden_static_files` is deprecated and will be removed in v1. + +### Tags + +#### 🚨📢 BREAKING CHANGES + +- `{% component_dependencies %}` tag was removed. Instead, use `{% component_js_dependencies %}` and `{% component_css_dependencies %}` + + - The combined tag was removed to encourage the best practice of putting JS scripts at the end of ``, and CSS styles inside ``. + + On the other hand, co-locating JS script and CSS styles can lead to + a [flash of unstyled content](https://en.wikipedia.org/wiki/Flash_of_unstyled_content), + as either JS scripts will block the rendering, or CSS will load too late. + +- The undocumented keyword arg `preload` of `{% component_js_dependencies %}` and `{% component_css_dependencies %}` tags was removed. + This will be replaced with HTML fragment support. + +#### Fix + +- Allow using forward slash (`/`) when defining custom TagFormatter, + e.g. `{% MyComp %}..{% /MyComp %}`. + +#### Refactor + +- `{% component_dependencies %}` tags are now OPTIONAL - If your components use JS and CSS, but you don't use `{% component_dependencies %}` tags, the JS and CSS will now be, by default, inserted at the end of `` and at the end of `` respectively. + +### Slots + +#### Feat + +- Fills can now be defined within loops (`{% for %}`) or other tags (like `{% with %}`), + or even other templates using `{% include %}`. + + Following is now possible + + ```django + {% component "table" %} + {% for slot_name in slots %} + {% fill name=slot_name %} + {% endfill %} + {% endfor %} + {% endcomponent %} + ``` + +- If you need to access the data or the default content of a default fill, you can + set the `name` kwarg to `"default"`. + + Previously, a default fill would be defined simply by omitting the `{% fill %}` tags: + + ```django + {% component "child" %} + Hello world + {% endcomponent %} + ``` + + But in that case you could not access the slot data or the default content, like it's possible + for named fills: + + ```django + {% component "child" %} + {% fill name="header" data="data" %} + Hello {{ data.user.name }} + {% endfill %} + {% endcomponent %} + ``` + + Now, you can specify default tag by using `name="default"`: + + ```django + {% component "child" %} + {% fill name="default" data="data" %} + Hello {{ data.user.name }} + {% endfill %} + {% endcomponent %} + ``` + +- When inside `get_context_data()` or other component methods, the default fill + can now be accessed as `Component.input.slots["default"]`, e.g.: + + ```py + class MyTable(Component): + def get_context_data(self, *args, **kwargs): + default_slot = self.input.slots["default"] + ... + ``` + +- You can now dynamically pass all slots to a child component. This is similar to + [passing all slots in Vue](https://vue-land.github.io/faq/forwarding-slots#passing-all-slots): + + ```py + class MyTable(Component): + def get_context_data(self, *args, **kwargs): + return { + "slots": self.input.slots, + } + + template: """ +
+ {% component "child" %} + {% for slot_name in slots %} + {% fill name=slot_name data="data" %} + {% slot name=slot_name ...data / %} + {% endfill %} + {% endfor %} + {% endcomponent %} +
+ """ + ``` + +#### Fix + +- Slots defined with `{% fill %}` tags are now properly accessible via `self.input.slots` in `get_context_data()` + +- Do not raise error if multiple slots with same name are flagged as default + +- Slots can now be defined within loops (`{% for %}`) or other tags (like `{% with %}`), + or even other templates using `{% include %}`. + + Previously, following would cause the kwarg `name` to be an empty string: + + ```django + {% for slot_name in slots %} + {% slot name=slot_name %} + {% endfor %} + ``` + +#### Refactor + +- When you define multiple slots with the same name inside a template, + you now have to set the `default` and `required` flags individually. + + ```htmldjango +
+
+ {% slot "image" default required %}Image here{% endslot %} +
+
+ {% slot "image" default required %}Image here{% endslot %} +
+
+ ``` + + This means you can also have multiple slots with the same name but + different conditions. + + E.g. in this example, we have a component that renders a user avatar + - a small circular image with a profile picture of name initials. + + If the component is given `image_src` or `name_initials` variables, + the `image` slot is optional. But if neither of those are provided, + you MUST fill the `image` slot. + + ```htmldjango +
+ {% if image_src %} + {% slot "image" default %} + + {% endslot %} + {% elif name_initials %} + {% slot "image" default required %} +
+ {{ name_initials }} +
+ {% endslot %} + {% else %} + {% slot "image" default required / %} + {% endif %} +
+ ``` + +- The slot fills that were passed to a component and which can be accessed as `Component.input.slots` + can now be passed through the Django template, e.g. as inputs to other tags. + + Internally, django-components handles slot fills as functions. + + Previously, if you tried to pass a slot fill within a template, Django would try to call it as a function. + + Now, something like this is possible: + + ```py + class MyTable(Component): + def get_context_data(self, *args, **kwargs): + return { + "child_slot": self.input.slots["child_slot"], + } + + template: """ +
+ {% component "child" content=child_slot / %} +
+ """ + ``` + + NOTE: Using `{% slot %}` and `{% fill %}` tags is still the preferred method, but the approach above + may be necessary in some complex or edge cases. + +- The `is_filled` variable (and the `{{ component_vars.is_filled }}` context variable) now returns + `False` when you try to access a slot name which has not been defined: + + Before: + + ```django + {{ component_vars.is_filled.header }} -> True + {{ component_vars.is_filled.footer }} -> False + {{ component_vars.is_filled.nonexist }} -> "" (empty string) + ``` + + After: + ```django + {{ component_vars.is_filled.header }} -> True + {{ component_vars.is_filled.footer }} -> False + {{ component_vars.is_filled.nonexist }} -> False + ``` + +- Components no longer raise an error if there are extra slot fills + +- Components will raise error when a slot is doubly-filled. + + E.g. if we have a component with a default slot: + + ```django + {% slot name="content" default / %} + ``` + + Now there is two ways how we can target this slot: Either using `name="default"` + or `name="content"`. + + In case you specify BOTH, the component will raise an error: + + ```django + {% component "child" %} + {% fill slot="default" %} + Hello from default slot + {% endfill %} + {% fill slot="content" data="data" %} + Hello from content slot + {% endfill %} + {% endcomponent %} + ``` + +## 🚨📢 v0.100 + +#### BREAKING CHANGES + +- `django_components.safer_staticfiles` app was removed. It is no longer needed. + +- Installation changes: + + - 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/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/django-components/django-components#app_dirs)). + +#### Refactor + +- When you call `as_view()` on a component instance, that instance will be passed to `View.as_view()` + +## v0.97 + +#### Fix + +- 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 + +- The previously undocumented `get_template` was made private. + +- In it's place, there's a new `get_template`, which supersedes `get_template_string` (will be removed in v1). The new `get_template` is the same as `get_template_string`, except + it allows to return either a string or a Template instance. + +- You now must use only one of `template`, `get_template`, `template_name`, or `get_template_name`. + +## v0.96 + +#### 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/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/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` + +## 0.95 + +#### Feat + +- 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 + +- Changed `Component.input` to raise `RuntimeError` if accessed outside of render context. Previously it returned `None` if unset. + +## v0.94 + +#### Feat + +- 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/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/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/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 + +- Component library authors can now configure `CONTEXT_BEHAVIOR` and `TAG_FORMATTER` settings independently from user settings. + +## 🚨📢 v0.92 + +#### 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/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/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/django-components/django-components#adding-type-hints-with-generics)) + +## v0.90 + +#### Feat + +- All tags (`component`, `slot`, `fill`, ...) now support "self-closing" or "inline" form, where you can omit the closing tag: + + ```django + {# Before #} + {% component "button" %}{% endcomponent %} + {# After #} + {% component "button" / %} + ``` + +- All tags now support the "dictionary key" or "aggregate" syntax (`kwarg:key=val`): + + ```django + {% component "button" attrs:class="hidden" %} + ``` + +- 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`: + + ```django + {% component "button" href="..." disabled %} + Click me! + {% endcomponent %} + ``` + + While `django_components.shorthand_component_formatter` allows you to write components like so: + + ```django + {% button href="..." disabled %} + Click me! + {% endbutton %} + ``` + +## 🚨📢 v0.85 + +#### BREAKING CHANGES + +- Autodiscovery module resolution changed. Following undocumented behavior was removed: + + - Previously, autodiscovery also imported any `[app]/components.py` files, and used `SETTINGS_MODULE` to search for component dirs. + + To migrate from: + + - `[app]/components.py` - Define each module in `COMPONENTS.libraries` setting, + or import each module inside the `AppConfig.ready()` hook in respective `apps.py` files. + + - `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.2/ref/settings/#std-setting-STATICFILES_DIRS)). + +## 🚨📢 v0.81 + +#### BREAKING CHANGES + +- The order of arguments to `render_to_response` has changed, to align with the (now public) `render` method of `Component` class. + +#### Feat + +- `Component.render()` is public and documented + +- Slots passed `render_to_response` and `render` can now be rendered also as functions. + +## v0.80 + +#### Feat + +- Vue-like provide/inject with the `{% provide %}` tag and `inject()` method. + +## 🚨📢 v0.79 + +#### 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/django-components/django-components/issues/498). + +## 🚨📢 v0.77 + +#### BREAKING + +- The syntax for accessing default slot content has changed from + + ```django + {% fill "my_slot" as "alias" %} + {{ alias.default }} + {% endfill %} + + ``` + + to + + ```django + {% fill "my_slot" default="alias" %} + {{ alias }} + {% endfill %} + ``` + +## v0.74 + +#### Feat + +- `{% html_attrs %}` tag for formatting data as HTML attributes + +- `prefix:key=val` construct for passing dicts to components + +## 🚨📢 v0.70 + +#### BREAKING CHANGES + +- `{% 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/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/django-components/django-components/tree/0.67#isolate-components-slots) for more details. + +## 🚨📢 v0.50 + +#### BREAKING CHANGES + +- `{% component_block %}` is now `{% component %}`, and `{% component %}` blocks need an ending `{% endcomponent %}` tag. + + The new `python manage.py upgradecomponent` command can be used to upgrade a directory (use `--path` argument to point to each dir) of templates that use components to the new syntax automatically. + + This change is done to simplify the API in anticipation of a 1.0 release of django_components. After 1.0 we intend to be stricter with big changes like this in point releases. + +## v0.34 + +#### Feat + +- 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 + +#### Feat + +- 'implicit' slot filling and the `default` option for `slot` tags. + +## v0.27 + +#### 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/django-components/django-components#security-notes)). + +## 🚨📢 v0.26 + +#### BREAKING CHANGES + +- Changed the syntax for `{% slot %}` tags. From now on, we separate defining a slot (`{% slot %}`) from filling a slot with content (`{% fill %}`). This means you will likely need to change a lot of slot tags to fill. + + We understand this is annoying, but it's the only way we can get support for nested slots that fill in other slots, which is a very nice feature to have access to. Hoping that this will feel worth it! + +## v0.22 + +#### Feat + +- All files inside components subdirectores are autoimported to simplify setup. + + An existing project might start to get `AlreadyRegistered` errors because of this. To solve this, either remove your custom loading of components, or set `"autodiscover": False` in `settings.COMPONENTS`. + +## v0.17 + +#### BREAKING CHANGES + +- Renamed `Component.context` and `Component.template` to `get_context_data` and `get_template_name`. The old methods still work, but emit a deprecation warning. + + This change was done to sync naming with Django's class based views, and make using django-components more familiar to Django users. `Component.context` and `Component.template` will be removed when version 1.0 is released. diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 00000000..8b126249 --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,5 @@ +# MANIFEST.in is defined so we can include non-Python (e.g. JS) files +# in the built distribution. +# See https://setuptools.pypa.io/en/latest/userguide/miscellaneous.html +graft src/django_components/static +prune tests diff --git a/README.md b/README.md index 7a2061dc..8df3262e 100644 --- a/README.md +++ b/README.md @@ -1,3356 +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://EmilStenstrom.github.io/django-components/latest/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/)
-Create simple reusable template components in Django +`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. - +With `django-components` you can support Django projects small and large without leaving the Django ecosystem. -- ✨ **Reusable components**: Create components that can be reused in different parts of your project, or even in different projects. -- 📁 **Single file components**: Keep your Python, CSS, Javascript and HTML in one place (if you wish) -- 🎰 **Slots**: Define slots in your components to make them more flexible. -- 💻 **CLI**: A command line interface to help you create new components. -- 🚀 **Wide compatibility**: Works with [modern and LTS versions of Django](https://emilstenstrom.github.io/django-components/latest/user_guide/requirements_compatibility). -- **Load assets**: Automatically load the right CSS and Javascript files for your components, with [our middleware](https://emilstenstrom.github.io/django-components/latest/user_guide/creating_using_components/middleware). +## Quickstart +A component in django-components can be as simple as a Django template and Python code to declare the component: -## Summary - -It lets you create "template components", that contains both the template, the Javascript and the CSS needed to generate the front end code you need for a modern app. Use components like this: - -```htmldjango -{% component "calendar" date="2015-06-19" %}{% endcomponent %} +```django +{# components/calendar/calendar.html #} +
+ Today's date is {{ date }} +
``` -And this is what gets rendered (plus the CSS and Javascript you've specified): +```py +# components/calendar/calendar.py +from django_components import Component, register -```html -
Today's date is 2015-06-19
+@register("calendar") +class Calendar(Component): + template_file = "calendar.html" ``` -[See the example project](./sampleproject) or read on to learn about the details! +Or a combination of Django template, Python, CSS, and Javascript: -## Table of Contents - -- [Release notes](#release-notes) -- [Security notes 🚨](#security-notes-) -- [Installation](#installation) -- [Compatibility](#compatibility) -- [Create your first component](#create-your-first-component) -- [Using single-file components](#using-single-file-components) -- [Use components in templates](#use-components-in-templates) -- [Use components outside of templates](#use-components-outside-of-templates) -- [Registering components](#registering-components) -- [Use components as views](#use-components-as-views) -- [Autodiscovery](#autodiscovery) -- [Using slots in templates](#using-slots-in-templates) -- [Accessing data passed to the component](#accessing-data-passed-to-the-component) -- [Rendering HTML attributes](#rendering-html-attributes) -- [Template tag syntax](#template-tag-syntax) -- [Prop drilling and dependency injection (provide / inject)](#prop-drilling-and-dependency-injection-provide--inject) -- [Component context and scope](#component-context-and-scope) -- [Customizing component tags with TagFormatter](#customizing-component-tags-with-tagformatter) -- [Defining HTML/JS/CSS files](#defining-htmljscss-files) -- [Rendering JS/CSS dependencies](#rendering-jscss-dependencies) -- [Available settings](#available-settings) -- [Logging and debugging](#logging-and-debugging) -- [Management Command](#management-command) -- [Writing and sharing component libraries](#writing-and-sharing-component-libraries) -- [Community examples](#community-examples) -- [Running django-components project locally](#running-django-components-project-locally) -- [Development guides](#development-guides) - -## Release notes - -**Version 0.93** -- Spread operator `...dict` inside template tags. See [Spread operator](#spread-operator)) -- Use template tags inside string literals in component inputs. See [Use template tags inside component inputs](#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 -- Component library authors can now configure `CONTEXT_BEHAVIOR` and `TAG_FORMATTER` settings independently from user settings. - -🚨📢 **Version 0.92** -- BREAKING CHANGE: `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](#modifying-the-view-class)) - -- The inputs (args, kwargs, slots, context, ...) that you pass to `Component.render()` can be accessed from within `get_context_data`, `get_template_string` and `get_template_name` via `self.input`. (See [Accessing data passed to the component](#accessing-data-passed-to-the-component)) - -- Typing: `Component` class supports generics that specify types for `Component.render` (See [Adding type hints with Generics](#adding-type-hints-with-generics)) - -**Version 0.90** -- All tags (`component`, `slot`, `fill`, ...) now support "self-closing" or "inline" form, where you can omit the closing tag: - ```django - {# Before #} - {% component "button" %}{% endcomponent %} - {# After #} - {% component "button" / %} - ``` -- All tags now support the "dictionary key" or "aggregate" syntax (`kwarg:key=val`): - ```django - {% component "button" attrs:class="hidden" %} - ``` -- You can change how the components are written in the template with [TagFormatter](#customizing-component-tags-with-tagformatter). - - The default is `django_components.component_formatter`: - ```django - {% component "button" href="..." disabled %} - Click me! - {% endcomponent %} - ``` - - While `django_components.shorthand_component_formatter` allows you to write components like so: - - ```django - {% button href="..." disabled %} - Click me! - {% endbutton %} - -🚨📢 **Version 0.85** Autodiscovery module resolution changed. Following undocumented behavior was removed: - -- Previously, autodiscovery also imported any `[app]/components.py` files, and used `SETTINGS_MODULE` to search for component dirs. - - To migrate from: - - `[app]/components.py` - Define each module in `COMPONENTS.libraries` setting, - or import each module inside the `AppConfig.ready()` hook in respective `apps.py` files. - - `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)). - -🚨📢 **Version 0.81** Aligned the `render_to_response` method with the (now public) `render` method of `Component` class. Moreover, slots passed to these can now be rendered also as functions. - -- BREAKING CHANGE: The order of arguments to `render_to_response` has changed. - -**Version 0.80** introduces dependency injection with the `{% provide %}` tag and `inject()` method. - -🚨📢 **Version 0.79** - -- BREAKING CHANGE: 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). - -🚨📢 **Version 0.77** CHANGED the syntax for accessing default slot content. - -- Previously, the syntax was - `{% fill "my_slot" as "alias" %}` and `{{ alias.default }}`. -- Now, the syntax is - `{% fill "my_slot" default="alias" %}` and `{{ alias }}`. - -**Version 0.74** introduces `html_attrs` tag and `prefix:key=val` construct for passing dicts to components. - -🚨📢 **Version 0.70** - -- `{% 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](#context-behavior) for more details. - -**Version 0.67** 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. - -🚨📢 **Version 0.5** CHANGES THE SYNTAX for components. `component_block` is now `component`, and `component` blocks need an ending `endcomponent` tag. The new `python manage.py upgradecomponent` command can be used to upgrade a directory (use --path argument to point to each dir) of templates that use components to the new syntax automatically. - -This change is done to simplify the API in anticipation of a 1.0 release of django_components. After 1.0 we intend to be stricter with big changes like this in point releases. - -**Version 0.34** adds components as views, which allows you to handle requests and render responses from within a component. See the [documentation](#use-components-as-views) for more details. - -**Version 0.28** introduces 'implicit' slot filling and the `default` option for `slot` tags. - -**Version 0.27** adds 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). - -**Version 0.26** changes the syntax for `{% slot %}` tags. From now on, we separate defining a slot (`{% slot %}`) from filling a slot with content (`{% fill %}`). This means you will likely need to change a lot of slot tags to fill. We understand this is annoying, but it's the only way we can get support for nested slots that fill in other slots, which is a very nice featuPpre to have access to. Hoping that this will feel worth it! - -**Version 0.22** starts autoimporting all files inside components subdirectores, to simplify setup. An existing project might start to get AlreadyRegistered-errors because of this. To solve this, either remove your custom loading of components, or set "autodiscover": False in settings.COMPONENTS. - -**Version 0.17** renames `Component.context` and `Component.template` to `get_context_data` and `get_template_name`. The old methods still work, but emit a deprecation warning. This change was done to sync naming with Django's class based views, and make using django-components more familiar to Django users. `Component.context` and `Component.template` will be removed when version 1.0 is released. - -## Security notes 🚨 - -_You are advised to read this section before using django-components in production._ - -### Static files - -Components can be organized however you prefer. -That said, our prefered way is to keep the files of a component close together by bundling them in the same directory. -This means that files containing backend logic, such as Python modules and HTML templates, live in the same directory as static files, e.g. JS and CSS. - -If your are using _django.contrib.staticfiles_ to collect static files, no distinction is made between the different kinds of files. -As a result, your Python code and templates may inadvertently become available on your static file server. -You probably don't want this, as parts of your backend logic will be exposed, posing a **potential security vulnerability**. - -As of _v0.27_, django-components ships with an additional installable app _django_components.**safer_staticfiles**_. -It is a drop-in replacement for _django.contrib.staticfiles_. -Its behavior is 100% identical except it ignores .py and .html files, meaning these will not end up on your static files server. -To use it, add it to INSTALLED_APPS and remove _django.contrib.staticfiles_. - -```python -INSTALLED_APPS = [ - # 'django.contrib.staticfiles', # <-- REMOVE - 'django_components', - 'django_components.safer_staticfiles' # <-- ADD -] +```django +{# components/calendar/calendar.html #} +
+ Today's date is {{ date }} +
``` -If you are on an older version of django-components, your alternatives are a) passing `--ignore ` options to the _collecstatic_ CLI command, or b) defining a subclass of StaticFilesConfig. -Both routes are described in the official [docs of the _staticfiles_ app](https://docs.djangoproject.com/en/4.2/ref/contrib/staticfiles/#customizing-the-ignored-pattern-list). - -Note that `safer_staticfiles` excludes the `.py` and `.html` files for [collectstatic command](https://docs.djangoproject.com/en/5.0/ref/contrib/staticfiles/#collectstatic): - -```sh -python manage.py collectstatic -``` - -but it is ignored on the [development server](https://docs.djangoproject.com/en/5.0/ref/django-admin/#runserver): - -```sh -python manage.py runserver -``` - -For a step-by-step guide on deploying production server with static files, -[see the demo project](./sampleproject/README.md). - -## Installation - -1. Install the app into your environment: - - > `pip install django_components` - -2. Then add the app into `INSTALLED_APPS` in settings.py - - ```python - INSTALLED_APPS = [ - ..., - 'django_components', - ] - ``` - -3. Ensure that `BASE_DIR` setting is defined in settings.py: - - ```py - BASE_DIR = Path(__file__).resolve().parent.parent - ``` - -4. Modify `TEMPLATES` section of settings.py as follows: - - - _Remove `'APP_DIRS': True,`_ - - Add `loaders` to `OPTIONS` list and set it to following value: - - ```python - TEMPLATES = [ - { - ..., - 'OPTIONS': { - 'context_processors': [ - ... - ], - 'loaders':[( - 'django.template.loaders.cached.Loader', [ - 'django.template.loaders.filesystem.Loader', - 'django.template.loaders.app_directories.Loader', - 'django_components.template_loader.Loader', - ] - )], - }, - }, - ] - ``` - -5. Modify `STATICFILES_DIRS` (or add it if you don't have it) so django can find your static JS and CSS files: - - ```python - STATICFILES_DIRS = [ - ..., - os.path.join(BASE_DIR, "components"), - ] - ``` - - If `STATICFILES_DIRS` is omitted or empty, django-components will by default look for - `{BASE_DIR}/components` - - NOTE: The paths in `STATICFILES_DIRS` must be full paths. [See Django docs](https://docs.djangoproject.com/en/5.0/ref/settings/#staticfiles-dirs). - -### Optional - -To avoid loading the app in each template using `{% load component_tags %}`, you can add the tag as a 'builtin' in settings.py - -```python -TEMPLATES = [ - { - ..., - 'OPTIONS': { - 'context_processors': [ - ... - ], - 'builtins': [ - 'django_components.templatetags.component_tags', - ] - }, - }, -] -``` - -Read on to find out how to build your first component! - -## Compatibility - -Django-components supports all supported combinations versions of [Django](https://docs.djangoproject.com/en/dev/faq/install/#what-python-version-can-i-use-with-django) and [Python](https://devguide.python.org/versions/#versions). - -| Python version | Django version | -| -------------- | -------------- | -| 3.8 | 4.2 | -| 3.9 | 4.2 | -| 3.10 | 4.2, 5.0 | -| 3.11 | 4.2, 5.0 | -| 3.12 | 4.2, 5.0 | - -## Create your first component - -A component in django-components is the combination of four things: CSS, Javascript, a Django template, and some Python code to put them all together. - -``` - sampleproject/ - ├── calendarapp/ - ├── components/ 🆕 - │ └── calendar/ 🆕 - │ ├── calendar.py 🆕 - │ ├── script.js 🆕 - │ ├── style.css 🆕 - │ └── template.html 🆕 - ├── sampleproject/ - ├── manage.py - └── requirements.txt -``` - -Start by creating empty files in the structure above. - -First, you need a CSS file. Be sure to prefix all rules with a unique class so they don't clash with other rules. - -```css title="[project root]/components/calendar/style.css" -/* In a file called [project root]/components/calendar/style.css */ -.calendar-component { +```css +/* components/calendar/calendar.css */ +.calendar { width: 200px; background: pink; } -.calendar-component span { - font-weight: bold; -} ``` -Then you need a javascript file that specifies how you interact with this component. You are free to use any javascript framework you want. A good way to make sure this component doesn't clash with other components is to define all code inside an anonymous function that calls itself. This makes all variables defined only be defined inside this component and not affect other components. - -```js title="[project root]/components/calendar/script.js" -/* In a file called [project root]/components/calendar/script.js */ -(function () { - if (document.querySelector(".calendar-component")) { - document.querySelector(".calendar-component").onclick = function () { - alert("Clicked calendar!"); - }; - } -})(); +```js +/* components/calendar/calendar.js */ +document.querySelector(".calendar").onclick = () => { + alert("Clicked calendar!"); +}; ``` -Now you need a Django template for your component. Feel free to define more variables like `date` in this example. When creating an instance of this component we will send in the values for these variables. The template will be rendered with whatever template backend you've specified in your Django settings file. - -```htmldjango title="[project root]/components/calendar/calendar.html" -{# In a file called [project root]/components/calendar/template.html #} -
Today's date is {{ date }}
-``` - -Finally, we use django-components to tie this together. Start by creating a file called `calendar.py` in your component calendar directory. It will be auto-detected and loaded by the app. - -Inside this file we create a Component by inheriting from the Component class and specifying the context method. We also register the global component registry so that we easily can render it anywhere in our templates. - -```python title="[project root]/components/calendar/calendar.py" -# In a file called [project root]/components/calendar/calendar.py +```py +# components/calendar/calendar.py from django_components import Component, register @register("calendar") class Calendar(Component): - # Templates inside `[your apps]/components` dir and `[project root]/components` dir - # will be automatically found. To customize which template to use based on context - # you can override method `get_template_name` instead of specifying `template_name`. - # - # `template_name` can be relative to dir where `calendar.py` is, or relative to STATICFILES_DIRS - template_name = "template.html" + template_file = "calendar.html" + js_file = "calendar.js" + css_file = "calendar.css" - # This component takes one parameter, a date string to show in the template - def get_context_data(self, date): - return { - "date": date, - } - - # Both `css` and `js` can be relative to dir where `calendar.py` is, or relative to STATICFILES_DIRS - class Media: - css = "style.css" - js = "script.js" + def get_template_data(self, args, kwargs, slots, context): + return {"date": kwargs["date"]} ``` -And voilá!! We've created our first component. +Use the component like this: -## Using single-file components - -Components can also be defined in a single file, which is useful for small components. To do this, you can use the `template`, `js`, and `css` class attributes instead of the `template_name` and `Media`. For example, here's the calendar component from above, defined in a single file: - -```python title="[project root]/components/calendar.py" -# In a file called [project root]/components/calendar.py -from django_components import Component, register, types - -@register("calendar") -class Calendar(Component): - def get_context_data(self, date): - return { - "date": date, - } - - template: types.django_html = """ -
Today's date is {{ date }}
- """ - - css: types.css = """ - .calendar-component { width: 200px; background: pink; } - .calendar-component span { font-weight: bold; } - """ - - js: types.js = """ - (function(){ - if (document.querySelector(".calendar-component")) { - document.querySelector(".calendar-component").onclick = function(){ alert("Clicked calendar!"); }; - } - })() - """ +```django +{% component "calendar" date="2024-11-06" %}{% endcomponent %} ``` -This makes it easy to create small components without having to create a separate template, CSS, and JS file. - -### Syntax highlight and code assistance - -#### VSCode - -Note, in the above example, that the `t.django_html`, `t.css`, and `t.js` types are used to specify the type of the template, CSS, and JS files, respectively. This is not necessary, but if you're using VSCode with the [Python Inline Source Syntax Highlighting](https://marketplace.visualstudio.com/items?itemName=samwillis.python-inline-source) extension, it will give you syntax highlighting for the template, CSS, and JS. - -#### Pycharm (or other Jetbrains IDEs) - -If you're a Pycharm user (or any other editor from Jetbrains), you can have coding assistance as well: - -```python -from django_components import Component, register - -@register("calendar") -class Calendar(Component): - def get_context_data(self, date): - return { - "date": date, - } - - # language=HTML - template= """ -
Today's date is {{ date }}
- """ - - # language=CSS - css = """ - .calendar-component { width: 200px; background: pink; } - .calendar-component span { font-weight: bold; } - """ - - # language=JS - js = """ - (function(){ - if (document.querySelector(".calendar-component")) { - document.querySelector(".calendar-component").onclick = function(){ alert("Clicked calendar!"); }; - } - })() - """ -``` - -You don't need to use `types.django_html`, `types.css`, `types.js` since Pycharm uses [language injections](https://www.jetbrains.com/help/pycharm/using-language-injections.html). -You only need to write the comments `# language=` above the variables. - -## Use components in templates - -First load the `component_tags` tag library, then use the `component_[js/css]_dependencies` and `component` tags to render the component to the page. - -```htmldjango -{% load component_tags %} - - - - My example calendar - {% component_css_dependencies %} - - - {% component "calendar" date="2015-06-19" %}{% endcomponent %} - {% component_js_dependencies %} - - -``` - -> NOTE: Instead of writing `{% endcomponent %}` at the end, you can use a self-closing tag: -> -> `{% component "calendar" date="2015-06-19" / %}` - -The output from the above template will be: +And this is what gets rendered: ```html - - - - My example calendar - - - -
- Today's date is 2015-06-19 -
- - - - +
+ Today's date is 2024-11-06 +
``` -This makes it possible to organize your front-end around reusable components. Instead of relying on template tags and keeping your CSS and Javascript in the static directory. +Read on to learn about all the exciting details and configuration possibilities! -## Use components outside of templates +(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)) -_New in version 0.81_ +## Features -Components can be rendered outside of Django templates, calling them as regular functions ("React-style"). +### Modern and modular UI -The component class defines `render` and `render_to_response` class methods. These methods accept positional args, kwargs, and slots, offering the same flexibility as the `{% component %}` tag: - -```py -class SimpleComponent(Component): - template = """ - {% load component_tags %} - hello: {{ hello }} - foo: {{ foo }} - kwargs: {{ kwargs|safe }} - slot_first: {% slot "first" required / %} - """ - - def get_context_data(self, arg1, arg2, **kwargs): - return { - "hello": arg1, - "foo": arg2, - "kwargs": kwargs, - } - -rendered = SimpleComponent.render( - args=["world", "bar"], - kwargs={"kw1": "test", "kw2": "ooo"}, - slots={"first": "FIRST_SLOT"}, - context={"from_context": 98}, -) -``` - -Renders: - -``` -hello: world -foo: bar -kwargs: {'kw1': 'test', 'kw2': 'ooo'} -slot_first: FIRST_SLOT -``` - -### Inputs of `render` and `render_to_response` - -Both `render` and `render_to_response` accept the same input: - -```py -Component.render( - context: Mapping | django.template.Context | None = None, - args: List[Any] | None = None, - kwargs: Dict[str, Any] | None = None, - slots: Dict[str, str | SafeString | SlotFunc] | None = None, - escape_slots_content: bool = True -) -> str: -``` - -- _`args`_ - Positional args for the component. This is the same as calling the component - as `{% component "my_comp" arg1 arg2 ... %}` - -- _`kwargs`_ - Keyword args for the component. This is the same as calling the component - as `{% component "my_comp" key1=val1 key2=val2 ... %}` - -- _`slots`_ - Component slot fills. This is the same as pasing `{% fill %}` tags to the component. - Accepts a dictionary of `{ slot_name: slot_content }` where `slot_content` can be a string - or [`SlotFunc`](#slotfunc). - -- _`escape_slots_content`_ - Whether the content from `slots` should be escaped. `True` by default to prevent XSS attacks. If you disable escaping, you should make sure that any content you pass to the slots is safe, especially if it comes from user input. - -- _`context`_ - A context (dictionary or Django's Context) within which the component - is rendered. The keys on the context can be accessed from within the template. - - NOTE: In "isolated" mode, context is NOT accessible, and data MUST be passed via - component's args and kwargs. - -#### `SlotFunc` - -When rendering components with slots in `render` or `render_to_response`, you can pass either a string or a function. - -The function has following signature: - -```py -def render_func( - context: Context, - data: Dict[str, Any], - slot_ref: SlotRef, -) -> str | SafeString: - return nodelist.render(ctx) -``` - -- _`context`_ - Django's Context available to the Slot Node. -- _`data`_ - Data passed to the `{% slot %}` tag. See [Scoped Slots](#scoped-slots). -- _`slot_ref`_ - The default slot content. See [Accessing original content of slots](#accessing-original-content-of-slots). - - NOTE: The slot is lazily evaluated. To render the slot, convert it to string with `str(slot_ref)`. - -Example: - -```py -def footer_slot(ctx, data, slot_ref): - return f""" - SLOT_DATA: {data['abc']} - ORIGINAL: {slot_ref} - """ - -MyComponent.render_to_response( - slots={ - "footer": footer_slot, - }, -) -``` - -### Adding type hints with Generics - -The `Component` class optionally accepts type parameters -that allow you to specify the types of args, kwargs, slots, and -data. - -```py -from typing import NotRequired, Tuple, TypedDict, SlotFunc - -# Positional inputs - Tuple -Args = Tuple[int, str] - -# Kwargs inputs - Mapping -class Kwargs(TypedDict): - variable: str - another: int - maybe_var: NotRequired[int] - -# Data returned from `get_context_data` - Mapping -class Data(TypedDict): - variable: str - -# The data available to the `my_slot` scoped slot -class MySlotData(TypedDict): - value: int - -# Slot functions - Mapping -class Slots(TypedDict): - # Use SlotFunc for slot functions. - # The generic specifies the `data` dictionary - my_slot: NotRequired[SlotFunc[MySlotData]] - -class Button(Component[Args, Kwargs, Data, Slots]): - def get_context_data(self, variable, another): - return { - "variable": variable, - } -``` - -When you then call `Component.render` or `Component.render_to_response`, you will get type hints: - -```py -Button.render( - # Error: First arg must be `int`, got `float` - args=(1.25, "abc"), - # Error: Key "another" is missing - kwargs={ - "variable": "text", - }, -) -``` - -### Response class of `render_to_response` - -While `render` method returns a plain string, `render_to_response` wraps the rendered content in a "Response" class. By default, this is `django.http.HttpResponse`. - -If you want to use a different Response class in `render_to_response`, set the `Component.response_class` attribute: - -```py -class MyResponse(HttpResponse): - def __init__(self, *args, **kwargs) -> None: - super().__init__(*args, **kwargs) - # Configure response - self.headers = ... - self.status = ... - -class SimpleComponent(Component): - response_class = MyResponse - template: types.django_html = "HELLO" - -response = SimpleComponent.render_to_response() -assert isinstance(response, MyResponse) -``` - -## Use components as views - -_New in version 0.34_ - -_Note: Since 0.92, Component no longer subclasses View. To configure the View class, set the nested `Component.View` class_ - -Components can now be used as views: -- Components define the `Component.as_view()` class method that can be used the same as [`View.as_view()`](https://docs.djangoproject.com/en/5.1/ref/class-based-views/base/#django.views.generic.base.View.as_view). - -- By default, you can define GET, POST or other HTTP handlers directly on the Component, same as you do with [View](https://docs.djangoproject.com/en/5.1/ref/class-based-views/base/#view). For example, you can override `get` and `post` to handle GET and POST requests, respectively. - -- In addition, `Component` now has a [`render_to_response`](#inputs-of-render-and-render_to_response) method that renders the component template based on the provided context and slots' data and returns an `HttpResponse` object. - -### Component as view example - -Here's an example of a calendar component defined as a view: +- 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 -# In a file called [project root]/components/calendar.py -from django_components import Component, ComponentView, register +from django_components import Component @register("calendar") class Calendar(Component): - template = """ -
-
- {% slot "header" / %} -
-
- Today's date is {{ date }} -
+
+ Today's date is + {{ date }}
""" - # Handle GET requests - def get(self, request, *args, **kwargs): - context = { - "date": request.GET.get("date", "2020-06-06"), + css = """ + .calendar { + width: 200px; + background: pink; } - slots = { - "header": "Calendar header", - } - # Return HttpResponse with the rendered content - return self.render_to_response( - context=context, - slots=slots, - ) -``` + """ -Then, to use this component as a view, you should create a `urls.py` file in your components directory, and add a path to the component's view: + js = """ + document.querySelector(".calendar") + .addEventListener("click", () => { + alert("Clicked calendar!"); + }); + """ -```python -# In a file called [project root]/components/urls.py -from django.urls import path -from components.calendar.calendar import 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"] -urlpatterns = [ - path("calendar/", Calendar.as_view()), -] -``` - -`Component.as_view()` is a shorthand for calling [`View.as_view()`](https://docs.djangoproject.com/en/5.1/ref/class-based-views/base/#django.views.generic.base.View.as_view) and passing the component -instance as one of the arguments. - -Remember to add `__init__.py` to your components directory, so that Django can find the `urls.py` file. - -Finally, include the component's urls in your project's `urls.py` file: - -```python -# In a file called [project root]/urls.py -from django.urls import include, path - -urlpatterns = [ - path("components/", include("components.urls")), -] -``` - -Note: Slots content are automatically escaped by default to prevent XSS attacks. To disable escaping, set `escape_slots_content=False` in the `render_to_response` method. If you do so, you should make sure that any content you pass to the slots is safe, especially if it comes from user input. - -If you're planning on passing an HTML string, check Django's use of [`format_html`](https://docs.djangoproject.com/en/5.0/ref/utils/#django.utils.html.format_html) and [`mark_safe`](https://docs.djangoproject.com/en/5.0/ref/utils/#django.utils.safestring.mark_safe). - -### Modifying the View class - -The View class that handles the requests is defined on `Component.View`. - -When you define a GET or POST handlers on the `Component` class, like so: - -```py -class MyComponent(Component): - def get(self, request, *args, **kwargs): - return self.render_to_response( - context={ - "date": request.GET.get("date", "2020-06-06"), - }, - ) - - def post(self, request, *args, **kwargs) -> HttpResponse: - variable = request.POST.get("variable") - return self.render_to_response( - kwargs={"variable": variable} - ) -``` - -Then the request is still handled by `Component.View.get()` or `Component.View.post()` -methods. However, by default, `Component.View.get()` points to `Component.get()`, and so on. - -```py -class ComponentView(View): - component: Component = None - ... - - def get(self, request, *args, **kwargs): - return self.component.get(request, *args, **kwargs) - - def post(self, request, *args, **kwargs): - return self.component.post(request, *args, **kwargs) - - ... -``` - -If you want to define your own `View` class, you need to: -1. Set the class as `Component.View` -2. Subclass from `ComponentView`, so the View instance has access to the component instance. - -In the example below, we added extra logic into `View.setup()`. - -Note that the POST handler is still defined at the top. This is because `View` subclasses `ComponentView`, which defines the `post()` method that calls `Component.post()`. - -If you were to overwrite the `View.post()` method, then `Component.post()` would be ignored. - -```py -from django_components import Component, ComponentView - -class MyComponent(Component): - - def post(self, request, *args, **kwargs) -> HttpResponse: - variable = request.POST.get("variable") - return self.component.render_to_response( - kwargs={"variable": variable} - ) - - class View(ComponentView): - def setup(self, request, *args, **kwargs): - super(request, *args, **kwargs) - - do_something_extra(request, *args, **kwargs) -``` - -## Registering components - -In previous examples you could repeatedly see us using `@register()` to "register" -the components. In this section we dive deeper into what it actually means and how you can -manage (add or remove) components. - -As a reminder, we may have a component like this: - -```python -from django_components import Component, register - -@register("calendar") -class Calendar(Component): - template_name = "template.html" - - # This component takes one parameter, a date string to show in the template - def get_context_data(self, date): + # Variables available in the template + def get_template_data(self, args, kwargs, slots, context): return { - "date": date, + "date": kwargs["date"] } ``` -which we then render in the template as: +### 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 "calendar" date="1970-01-01" %} -{% endcomponent %} -``` - -As you can see, `@register` links up the component class -with the `{% component %}` template tag. So when the template tag comes across -a component called `"calendar"`, it can look up it's class and instantiate it. - -### What is ComponentRegistry - -The `@register` decorator is a shortcut for working with the `ComponentRegistry`. - -`ComponentRegistry` manages which components can be used in the template tags. - -Each `ComponentRegistry` instance is associated with an instance -of Django's `Library`. And Libraries are inserted into Django template -using the `{% load %}` tags. - -The `@register` decorator accepts an optional kwarg `registry`, which specifies, the `ComponentRegistry` to register components into. -If omitted, the default `ComponentRegistry` instance defined in django_components is used. - -```py -my_registry = ComponentRegistry() - -@register(registry=my_registry) -class MyComponent(Component): - ... -``` - -The default `ComponentRegistry` is associated with the `Library` that -you load when you call `{% load component_tags %}` inside your template, or when you -add `django_components.templatetags.component_tags` to the template builtins. - -So when you register or unregister a component to/from a component registry, -then behind the scenes the registry automatically adds/removes the component's -template tags to/from the Library, so you can call the component from within the templates -such as `{% component "my_comp" %}`. - -### Working with ComponentRegistry - -The default `ComponentRegistry` instance can be imported as: - -```py -from django_components import registry -``` - -You can use the registry to manually add/remove/get components: - -```py -from django_components import registry - -# Register components -registry.register("button", ButtonComponent) -registry.register("card", CardComponent) - -# Get all or single -registry.all() # {"button": ButtonComponent, "card": CardComponent} -registry.get("card") # CardComponent - -# Unregister single component -registry.unregister("card") - -# Unregister all components -registry.clear() -``` - -### Registering components to custom ComponentRegistry - -If you are writing a component library to be shared with others, you may want to manage your own instance of `ComponentRegistry` -and register components onto a different `Library` instance than the default one. - -The `Library` instance can be set at instantiation of `ComponentRegistry`. If omitted, -then the default Library instance from django_components is used. - -```py -from django.template import Library -from django_components import ComponentRegistry - -my_library = Library(...) -my_registry = ComponentRegistry(library=my_library) -``` - -When you have defined your own `ComponentRegistry`, you can either register the components -with `my_registry.register()`, or pass the registry to the `@component.register()` decorator -via the `registry` kwarg: - -```py -from path.to.my.registry import my_registry - -@register("my_component", registry=my_registry) -class MyComponent(Component): - ... -``` - -NOTE: The Library instance can be accessed under `library` attribute of `ComponentRegistry`. - -### ComponentRegistry settings - -When you are creating an instance of `ComponentRegistry`, you can define the components' behavior within the template. - -The registry accepts these settings: -- `CONTEXT_BEHAVIOR` -- `TAG_FORMATTER` - -```py -from django.template import Library -from django_components import ComponentRegistry, RegistrySettings - -register = library = django.template.Library() -comp_registry = ComponentRegistry( - library=library, - settings=RegistrySettings( - CONTEXT_BEHAVIOR="isolated", - TAG_FORMATTER="django_components.component_formatter", - ), -) -``` - -These settings are [the same as the ones you can set for django_components](#available-settings). - -In fact, when you set `COMPONENT.tag_formatter` or `COMPONENT.context_behavior`, these are forwarded to the default `ComponentRegistry`. - -This makes it possible to have multiple registries with different settings in one projects, and makes sharing of component libraries possible. - -## Autodiscovery - -Every component that you want to use in the template with the `{% component %}` tag needs to be registered with the ComponentRegistry. Normally, we use the `@register` decorator for that: - -```py -from django_components import Component, register - -@register("calendar") -class Calendar(Component): - ... -``` - -But for the component to be registered, the code needs to be executed - the file needs to be imported as a module. - -One way to do that is by importing all your components in `apps.py`: - -```py -from django.apps import AppConfig - -class MyAppConfig(AppConfig): - name = "my_app" - - def ready(self) -> None: - from components.card.card import Card - from components.list.list import List - from components.menu.menu import Menu - from components.button.button import Button - ... -``` - -However, there's a simpler way! - -By default, the Python files in the `STATICFILES_DIRS` directories are auto-imported in order to auto-register the components. - -Autodiscovery occurs when Django is loaded, during the `ready` hook of the `apps.py` file. - -If you are using autodiscovery, keep a few points in mind: - -- Avoid defining any logic on the module-level inside the `components` dir, that you would not want to run anyway. -- Components inside the auto-imported files still need to be registered with `@register()` -- Auto-imported component files must be valid Python modules, they must use suffix `.py`, and module name should follow [PEP-8](https://peps.python.org/pep-0008/#package-and-module-names). - -Autodiscovery can be disabled in the [settings](#autodiscover---toggle-autodiscovery). - -### Manually trigger autodiscovery - -Autodiscovery can be also triggered manually as a function call. This is useful if you want to run autodiscovery at a custom point of the lifecycle: - -```py -from django_components import autodiscover - -autodiscover() -``` - -## Using slots in templates - -_New in version 0.26_: - -- The `slot` tag now serves only to declare new slots inside the component template. - - To override the content of a declared slot, use the newly introduced `fill` tag instead. -- Whereas unfilled slots used to raise a warning, filling a slot is now optional by default. - - To indicate that a slot must be filled, the new `required` option should be added at the end of the `slot` tag. - ---- - -Components support something called 'slots'. -When a component is used inside another template, slots allow the parent template to override specific parts of the child component by passing in different content. -This mechanism makes components more reusable and composable. -This behavior is similar to [slots in Vue](https://vuejs.org/guide/components/slots.html). - -In the example below we introduce two block tags that work hand in hand to make this work. These are... - -- `{% slot %}`/`{% endslot %}`: Declares a new slot in the component template. -- `{% fill %}`/`{% endfill %}`: (Used inside a `component` tag pair.) Fills a declared slot with the specified content. - -Let's update our calendar component to support more customization. We'll add `slot` tag pairs to its template, _template.html_. - -```htmldjango -
-
- {% slot "header" %}Calendar header{% endslot %} -
-
- {% slot "body" %}Today's date is {{ date }}{% endslot %} -
-
-``` - -When using the component, you specify which slots you want to fill and where you want to use the defaults from the template. It looks like this: - -```htmldjango -{% component "calendar" date="2020-06-06" %} - {% fill "body" %}Can you believe it's already {{ date }}??{% endfill %} -{% endcomponent %} -``` - -Since the 'header' fill is unspecified, it's taken from the base template. If you put this in a template, and pass in `date=2020-06-06`, this is what gets rendered: - -```htmldjango -
-
- Calendar header -
-
- Can you believe it's already 2020-06-06?? -
-
-``` - -### Default slot - -_Added in version 0.28_ - -As you can see, component slots lets you write reusable containers that you fill in when you use a component. This makes for highly reusable components that can be used in different circumstances. - -It can become tedious to use `fill` tags everywhere, especially when you're using a component that declares only one slot. To make things easier, `slot` tags can be marked with an optional keyword: `default`. When added to the end of the tag (as shown below), this option lets you pass filling content directly in the body of a `component` tag pair – without using a `fill` tag. Choose carefully, though: a component template may contain at most one slot that is marked as `default`. The `default` option can be combined with other slot options, e.g. `required`. - -Here's the same example as before, except with default slots and implicit filling. - -The template: - -```htmldjango -
-
- {% slot "header" %}Calendar header{% endslot %} -
-
- {% slot "body" default %}Today's date is {{ date }}{% endslot %} -
-
-``` - -Including the component (notice how the `fill` tag is omitted): - -```htmldjango -{% component "calendar" date="2020-06-06" %} - Can you believe it's already {{ date }}?? -{% endcomponent %} -``` - -The rendered result (exactly the same as before): - -```html -
-
Calendar header
-
Can you believe it's already 2020-06-06??
-
-``` - -You may be tempted to combine implicit fills with explicit `fill` tags. This will not work. The following component template will raise an error when compiled. - -```htmldjango -{# DON'T DO THIS #} -{% component "calendar" date="2020-06-06" %} - {% fill "header" %}Totally new header!{% endfill %} - Can you believe it's already {{ date }}?? -{% endcomponent %} -``` - -By contrast, it is permitted to use `fill` tags in nested components, e.g.: - -```htmldjango -{% component "calendar" date="2020-06-06" %} - {% component "beautiful-box" %} - {% fill "content" %} Can you believe it's already {{ date }}?? {% endfill %} - {% endcomponent %} -{% endcomponent %} -``` - -This is fine too: - -```htmldjango -{% component "calendar" date="2020-06-06" %} +{% component "Layout" + bookmarks=bookmarks + breadcrumbs=breadcrumbs +%} {% fill "header" %} - {% component "calendar-header" %} - Super Special Calendar 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 %} ``` -### Render fill in multiple places +### Extended template tags -_Added in version 0.70_ +`django-components` is designed for flexibility, making working with templates a breeze. -You can render the same content in multiple places by defining multiple slots with -identical names: +It extends Django's template tags syntax with: -```htmldjango -
-
- {% slot "image" %}Image here{% endslot %} -
-
- {% slot "image" %}Image here{% endslot %} -
-
-``` - -So if used like: - -```htmldjango -{% component "calendar" date="2020-06-06" %} - {% fill "image" %} - - {% endfill %} -{% endcomponent %} -``` - -This renders: - -```htmldjango -
-
- -
-
- -
-
-``` - -#### Default and required slots - -If you use a slot multiple times, you can still mark the slot as `default` or `required`. -For that, you must mark ONLY ONE of the identical slots. - -We recommend to mark the first occurence for consistency, e.g.: - -```htmldjango -
-
- {% slot "image" default required %}Image here{% endslot %} -
-
- {% slot "image" %}Image here{% endslot %} -
-
-``` - -Which you can then use are regular default slot: - -```htmldjango -{% component "calendar" date="2020-06-06" %} - -{% endcomponent %} -``` - -### Accessing original content of slots - -_Added in version 0.26_ - -> NOTE: In version 0.77, the syntax was changed from -> -> ```django -> {% fill "my_slot" as "alias" %} {{ alias.default }} -> ``` -> -> to -> -> ```django -> {% fill "my_slot" default="slot_default" %} {{ slot_default }} -> ``` - -Sometimes you may want to keep the original slot, but only wrap or prepend/append content to it. To do so, you can access the default slot via the `default` kwarg. - -Similarly to the `data` attribute, you specify the variable name through which the default slot will be made available. - -For instance, let's say you're filling a slot called 'body'. To render the original slot, assign it to a variable using the `'default'` keyword. You then render this variable to insert the default content: - -```htmldjango -{% component "calendar" date="2020-06-06" %} - {% fill "body" default="body_default" %} - {{ body_default }}. Have a great day! - {% endfill %} -{% endcomponent %} -``` - -This produces: - -```htmldjango -
-
- Calendar header -
-
- Today's date is 2020-06-06. Have a great day! -
-
-``` - -### Conditional slots - -_Added in version 0.26._ - -> NOTE: In version 0.70, `{% if_filled %}` tags were replaced with `{{ component_vars.is_filled }}` variables. If your slot name contained special characters, see the section "Accessing slot names with special characters". - -In certain circumstances, you may want the behavior of slot filling to depend on -whether or not a particular slot is filled. - -For example, suppose we have the following component template: - -```htmldjango -
-
- {% slot "title" %}Title{% endslot %} -
-
- {% slot "subtitle" %}{# Optional subtitle #}{% endslot %} -
-
-``` - -By default the slot named 'subtitle' is empty. Yet when the component is used without -explicit fills, the div containing the slot is still rendered, as shown below: - -```html -
-
Title
-
-
-``` - -This may not be what you want. What if instead the outer 'subtitle' div should only -be included when the inner slot is in fact filled? - -The answer is to use the `{{ component_vars.is_filled. }}` variable. You can use this together with Django's `{% if/elif/else/endif %}` tags to define a block whose contents will be rendered only if the component slot with -the corresponding 'name' is filled. - -This is what our example looks like with `component_vars.is_filled`. - -```htmldjango -
-
- {% slot "title" %}Title{% endslot %} -
- {% if component_vars.is_filled.subtitle %} -
- {% slot "subtitle" %}{# Optional subtitle #}{% endslot %} -
- {% endif %} -
-``` - -Here's our example with more complex branching. - -```htmldjango -
-
- {% slot "title" %}Title{% endslot %} -
- {% if component_vars.is_filled.subtitle %} -
- {% slot "subtitle" %}{# Optional subtitle #}{% endslot %} -
- {% elif component_vars.is_filled.title %} - ... - {% elif component_vars.is_filled. %} - ... - {% endif %} -
-``` - -Sometimes you're not interested in whether a slot is filled, but rather that it _isn't_. -To negate the meaning of `component_vars.is_filled`, simply treat it as boolean and negate it with `not`: - -```htmldjango -{% if not component_vars.is_filled.subtitle %} -
- {% slot "subtitle" / %} -
-{% endif %} -``` - -#### Accessing `is_filled` of slot names with special characters - -To be able to access a slot name via `component_vars.is_filled`, the slot name needs to be composed of only alphanumeric characters and underscores (e.g. `this__isvalid_123`). - -However, you can still define slots with other special characters. In such case, the slot name in `component_vars.is_filled` is modified to replace all invalid characters into `_`. - -So a slot named `"my super-slot :)"` will be available as `component_vars.is_filled.my_super_slot___`. - -### Scoped slots - -_Added in version 0.76_: - -Consider a component with slot(s). This component may do some processing on the inputs, and then use the processed variable in the slot's default template: - -```py -@register("my_comp") -class MyComp(Component): - template = """ -
- {% slot "content" default %} - input: {{ input }} - {% endslot %} -
- """ - - def get_context_data(self, input): - processed_input = do_something(input) - return {"input": processed_input} -``` - -You may want to design a component so that users of your component can still access the `input` variable, so they don't have to recompute it. - -This behavior is called "scoped slots". This is inspired by [Vue scoped slots](https://vuejs.org/guide/components/slots.html#scoped-slots) and [scoped slots of django-web-components](https://github.com/Xzya/django-web-components/tree/master?tab=readme-ov-file#scoped-slots). - -Using scoped slots consists of two steps: - -1. Passing data to `slot` tag -2. Accessing data in `fill` tag - -#### Passing data to slots - -To pass the data to the `slot` tag, simply pass them as keyword attributes (`key=value`): - -```py -@register("my_comp") -class MyComp(Component): - template = """ -
- {% slot "content" default input=input %} - input: {{ input }} - {% endslot %} -
- """ - - def get_context_data(self, input): - processed_input = do_something(input) - return { - "input": processed_input, - } -``` - -#### Accessing slot data in fill - -Next, we head over to where we define a fill for this slot. Here, to access the slot data -we set the `data` attribute to the name of the variable through which we want to access -the slot data. In the example below, we set it to `data`: + +- 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 "my_comp" %} - {% fill "content" data="data" %} - {{ data.input }} - {% endfill %} -{% endcomponent %} +{% 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" +/ %} ``` -To access slot data on a default slot, you have to explictly define the `{% fill %}` tags. +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). -So this works: +Read more on [Custom template tags](https://django-components.github.io/django-components/latest/concepts/advanced/template_tags/). -```django -{% component "my_comp" %} - {% fill "content" data="data" %} - {{ data.input }} - {% endfill %} -{% endcomponent %} -``` +### Full programmatic access -While this does not: +When you render a component, you can access everything about the component: -```django -{% component "my_comp" data="data" %} - {{ data.input }} -{% endcomponent %} -``` +- 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) -Note: You cannot set the `data` attribute and -[`default` attribute)](#accessing-original-content-of-slots) -to the same name. This raises an error: +```python +class Table(Component): + js_file = "table.js" + css_file = "table.css" -```django -{% component "my_comp" %} - {% fill "content" data="slot_var" default="slot_var" %} - {{ slot_var.input }} - {% endfill %} -{% endcomponent %} -``` - -### Dynamic slots and fills - -Until now, we were declaring slot and fill names statically, as a string literal, e.g. - -```django -{% slot "content" / %} -``` - -However, sometimes you may want to generate slots based on the given input. One example of this is [a table component like that of Vuetify](https://vuetifyjs.com/en/api/v-data-table/), which creates a header and an item slots for each user-defined column. - -In django_components you can achieve the same, simply by using a variable (or a [template expression](#use-template-tags-inside-component-inputs)) instead of a string literal: - -```django - - - {% for header in headers %} - - {% endfor %} - -
- {% slot "header-{{ header.key }}" value=header.title %} - {{ header.title }} - {% endslot %} -
-``` - -When using the component, you can either set the fill explicitly: - -```django -{% component "table" headers=headers items=items %} - {% fill "header-name" data="data" %} - {{ data.value }} - {% endfill %} -{% endcomponent %} -``` - -Or also use a variable: - -```django -{% component "table" headers=headers items=items %} - {# Make only the active column bold #} - {% fill "header-{{ active_header_name }}" data="data" %} - {{ data.value }} - {% endfill %} -{% endcomponent %} -``` - -> NOTE: It's better to use static slot names whenever possible for clarity. The dynamic slot names should be reserved for advanced use only. - -Lastly, in rare cases, you can also pass the slot name via [the spread operator](#spread-operator). This is possible, because the slot name argument is actually a shortcut for a `name` keyword argument. - -So this: - -```django -{% slot "content" / %} -``` - -is the same as: - -```django -{% slot name="content" / %} -``` - -So it's possible to define a `name` key on a dictionary, and then spread that onto the slot tag: - -```django -{# slot_props = {"name": "content"} #} -{% slot ...slot_props / %} -``` - -## Accessing data passed to the component - -When you call `Component.render` or `Component.render_to_response`, the inputs to these methods can be accessed from within the instance under `self.input`. - -This means that you can use `self.input` inside: -- `get_context_data` -- `get_template_name` -- `get_template_string` - -`self.input` is defined only for the duration of `Component.render`, and returns `None` when called outside of this. - -`self.input` has the same fields as the input to `Component.render`: - -```py -class TestComponent(Component): - def get_context_data(self, var1, var2, variable, another, **attrs): - assert self.input.args == (123, "str") - assert self.input.kwargs == {"variable": "test", "another": 1} - assert self.input.slots == {"my_slot": "MY_SLOT"} - assert isinstance(self.input.context, Context) - - return { - "variable": variable, - } - -rendered = TestComponent.render( - kwargs={"variable": "test", "another": 1}, - args=(123, "str"), - slots={"my_slot": "MY_SLOT"}, -) -``` - -## Rendering HTML attributes - -_New in version 0.74_: - -You can use the `html_attrs` tag to render HTML attributes, given a dictionary -of values. - -So if you have a template: - -```django -
-
-``` - -You can simplify it with `html_attrs` tag: - -```django -
-
-``` - -where `attrs` is: - -```py -attrs = { - "class": classes, - "data-id": my_id, -} -``` - -This feature is inspired by [`merge_attrs` tag of django-web-components](https://github.com/Xzya/django-web-components/tree/master?tab=readme-ov-file#default--merged-attributes) and -["fallthrough attributes" feature of Vue](https://vuejs.org/guide/components/attrs). - -### Removing atttributes - -Attributes that are set to `None` or `False` are NOT rendered. - -So given this input: - -```py -attrs = { - "class": "text-green", - "required": False, - "data-id": None, -} -``` - -And template: - -```django -
-
-``` - -Then this renders: - -```html -
-``` - -### Boolean attributes - -In HTML, boolean attributes are usually rendered with no value. Consider the example below where the first button is disabled and the second is not: - -```html - -``` - -HTML rendering with `html_attrs` tag or `attributes_to_string` works the same way, where `key=True` is rendered simply as `key`, and `key=False` is not render at all. - -So given this input: - -```py -attrs = { - "disabled": True, - "autofocus": False, -} -``` - -And template: - -```django -
-
-``` - -Then this renders: - -```html -
-``` - -### Default attributes - -Sometimes you may want to specify default values for attributes. You can pass a second argument (or kwarg `defaults`) to set the defaults. - -```django -
- ... -
-``` - -In the example above, if `attrs` contains e.g. the `class` key, `html_attrs` will render: - -`class="{{ attrs.class }}"` - -Otherwise, `html_attrs` will render: - -`class="{{ defaults.class }}"` - -### Appending attributes - -For the `class` HTML attribute, it's common that we want to _join_ multiple values, -instead of overriding them. For example, if you're authoring a component, you may -want to ensure that the component will ALWAYS have a specific class. Yet, you may -want to allow users of your component to supply their own classes. - -We can achieve this by adding extra kwargs. These values -will be appended, instead of overwriting the previous value. - -So if we have a variable `attrs`: - -```py -attrs = { - "class": "my-class pa-4", -} -``` - -And on `html_attrs` tag, we set the key `class`: - -```django -
-
-``` - -Then these will be merged and rendered as: - -```html -
-``` - -To simplify merging of variables, you can supply the same key multiple times, and these will be all joined together: - -```django -{# my_var = "class-from-var text-red" #} -
-
-``` - -Renders: - -```html -
-``` - -### Rules for `html_attrs` - -1. Both `attrs` and `defaults` can be passed as positional args - - `{% html_attrs attrs defaults key=val %}` - - or as kwargs - - `{% html_attrs key=val defaults=defaults attrs=attrs %}` - -2. Both `attrs` and `defaults` are optional (can be omitted) - -3. Both `attrs` and `defaults` are dictionaries, and we can define them the same way [we define dictionaries for the `component` tag](#pass-dictonary-by-its-key-value-pairs). So either as `attrs=attrs` or `attrs:key=value`. - -4. All other kwargs are appended and can be repeated. - -### Examples for `html_attrs` - -Assuming that: - -```py -class_from_var = "from-var" - -attrs = { - "class": "from-attrs", - "type": "submit", -} - -defaults = { - "class": "from-defaults", - "role": "button", -} -``` - -Then: - -- Empty tag
- `{% html_attr %}` - - renders (empty string):
- ` ` - -- Only kwargs
- `{% html_attr class="some-class" class=class_from_var data-id="123" %}` - - renders:
- `class="some-class from-var" data-id="123"` - -- Only attrs
- `{% html_attr attrs %}` - - renders:
- `class="from-attrs" type="submit"` - -- Attrs as kwarg
- `{% html_attr attrs=attrs %}` - - renders:
- `class="from-attrs" type="submit"` - -- Only defaults (as kwarg)
- `{% html_attr defaults=defaults %}` - - renders:
- `class="from-defaults" role="button"` - -- Attrs using the `prefix:key=value` construct
- `{% html_attr attrs:class="from-attrs" attrs:type="submit" %}` - - renders:
- `class="from-attrs" type="submit"` - -- Defaults using the `prefix:key=value` construct
- `{% html_attr defaults:class="from-defaults" %}` - - renders:
- `class="from-defaults" role="button"` - -- All together (1) - attrs and defaults as positional args:
- `{% html_attrs attrs defaults class="added_class" class=class_from_var data-id=123 %}` - - renders:
- `class="from-attrs added_class from-var" type="submit" role="button" data-id=123` - -- All together (2) - attrs and defaults as kwargs args:
- `{% html_attrs class="added_class" class=class_from_var data-id=123 attrs=attrs defaults=defaults %}` - - renders:
- `class="from-attrs added_class from-var" type="submit" role="button" data-id=123` - -- All together (3) - mixed:
- `{% html_attrs attrs defaults:class="default-class" class="added_class" class=class_from_var data-id=123 %}` - - renders:
- `class="from-attrs added_class from-var" type="submit" data-id=123` - -### Full example for `html_attrs` - -```py -@register("my_comp") -class MyComp(Component): - template: t.django_html = """ -
- Today's date is {{ date }} + template = """ +
+ {{ variable }}
""" - def get_context_data(self, date: Date, attrs: dict): + 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 { - "date": date, - "attrs": attrs, - "class_from_var": "extra-class" + "variable": kwargs["variable"], } -@register("parent") -class Parent(Component): - template: t.django_html = """ - {% component "my_comp" - date=date - attrs:class="pa-0 border-solid border-red" - attrs:data-json=json_data - attrs:@click="(e) => onClick(e, 'from_parent')" - / %} - """ +# Access component's HTML / JS / CSS +Table.template +Table.js +Table.css - def get_context_data(self, date: Date): - return { - "date": datetime.now(), - "json_data": json.dumps({"value": 456}) - } +# Render the component +rendered = Table.render( + kwargs={"variable": "test", "another": 1}, + args=(123, "str"), + slots={"footer": "MY_FOOTER"}, +) ``` -Note: For readability, we've split the tags across multiple lines. +### Granular HTML attributes -Inside `MyComp`, we defined a default attribute +Use the [`{% html_attrs %}`](https://django-components.github.io/django-components/latest/concepts/fundamentals/html_attributes/) template tag to render HTML attributes. -`defaults:class="pa-4 text-red"` +It supports: -So if `attrs` includes key `class`, the default above will be ignored. - -`MyComp` also defines `class` key twice. It means that whether the `class` -attribute is taken from `attrs` or `defaults`, the two `class` values -will be appended to it. - -So by default, `MyComp` renders: - -```html -
...
-``` - -Next, let's consider what will be rendered when we call `MyComp` from `Parent` -component. - -`MyComp` accepts a `attrs` dictionary, that is passed to `html_attrs`, so the -contents of that dictionary are rendered as the HTML attributes. - -In `Parent`, we make use of passing dictionary key-value pairs as kwargs to define -individual attributes as if they were regular kwargs. - -So all kwargs that start with `attrs:` will be collected into an `attrs` dict. +- Defining attributes as whole dictionaries or keyword arguments +- Merging attributes from multiple sources +- Boolean attributes +- Appending attributes +- Removing attributes +- Defining default attributes ```django - attrs:class="pa-0 border-solid border-red" - attrs:data-json=json_data - attrs:@click="(e) => onClick(e, 'from_parent')" -``` - -And `get_context_data` of `MyComp` will receive `attrs` input with following keys: - -```py -attrs = { - "class": "pa-0 border-solid", - "data-json": '{"value": 456}', - "@click": "(e) => onClick(e, 'from_parent')", -} -``` - -`attrs["class"]` overrides the default value for `class`, whereas other keys -will be merged. - -So in the end `MyComp` will render: - -```html
- ... -
``` -### Rendering HTML attributes outside of templates +[`{% 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. -If you need to use serialize HTML attributes outside of Django template and the `html_attrs` tag, you can use `attributes_to_string`: +```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 -from django_components.attributes import attributes_to_string +# components/calendar/calendar.py +@register("calendar") +class Calendar(Component): + template_file = "calendar.html" -attrs = { - "class": "my-class text-red pa-4", - "data-id": 123, - "required": True, - "disabled": False, - "ignored-attr": None, -} + class View: + # Register Component with `urlpatterns` + public = True -attributes_to_string(attrs) -# 'class="my-class text-red pa-4" data-id="123" required' + # 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()) ``` -## Template tag syntax +### Provide / Inject -All template tags in django_component, like `{% component %}` or `{% slot %}`, and so on, -support extra syntax that makes it possible to write components like in Vue or React (JSX). +`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): -### Self-closing tags +- 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 -When you have a tag like `{% component %}` or `{% slot %}`, but it has no content, you can simply append a forward slash `/` at the end, instead of writing out the closing tags like `{% endcomponent %}` or `{% endslot %}`: - -So this: - -```django -{% component "button" %}{% endcomponent %} -``` - -becomes - -```django -{% component "button" / %} -``` - -### Special characters - -_New in version 0.71_: - -Keyword arguments can contain special characters `# @ . - _`, so keywords like -so are still valid: +Read more about [Provide / Inject](https://django-components.github.io/django-components/latest/concepts/advanced/provide_inject). ```django - {% component "calendar" my-date="2015-06-19" @click.native=do_something #some_id=True / %} + {% provide "theme" variant="light" %} + {% component "header" / %} + {% endprovide %} ``` -These can then be accessed inside `get_context_data` so: +```djc_py +@register("header") +class Header(Component): + template = "..." -```py -@register("calendar") -class Calendar(Component): - # Since # . @ - are not valid identifiers, we have to - # use `**kwargs` so the method can accept these args. - def get_context_data(self, **kwargs): + def get_template_data(self, args, kwargs, slots, context): + theme = self.inject("theme").variant return { - "date": kwargs["my-date"], - "id": kwargs["#some_id"], - "on_click": kwargs["@click.native"] + "theme": theme, } ``` -### Spread operator +### Input validation and static type hints -_New in version 0.93_: +Avoid needless errors with [type hints and runtime input validation](https://django-components.github.io/django-components/latest/concepts/fundamentals/typing_and_validation/). -Instead of passing keyword arguments one-by-one: - -```django -{% component "calendar" title="How to abc" date="2015-06-19" author="John Wick" / %} -``` - -You can use a spread operator `...dict` to apply key-value pairs from a dictionary: +To opt-in to input validation, define types for component's args, kwargs, slots, and more: ```py -post_data = { - "title": "How to...", - "date": "2015-06-19", - "author": "John Wick", -} +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] ``` -```django -{% component "calendar" ...post_data / %} -``` - -This behaves similar to [JSX's spread operator](https://kevinyckim33.medium.com/jsx-spread-operator-component-props-meaning-3c9bcadd2493) -or [Vue's `v-bind`](https://vuejs.org/api/built-in-directives.html#v-bind). - -Spread operators are treated as keyword arguments, which means that: -1. Spread operators must come after positional arguments. -2. You cannot use spread operators for [positional-only arguments](https://martinxpn.medium.com/positional-only-and-keyword-only-arguments-in-python-37-100-days-of-python-310c311657b0). - -Other than that, you can use spread operators multiple times, and even put keyword arguments in-between or after them: - -```django -{% component "calendar" ...post_data id=post.id ...extra / %} -``` - -In a case of conflicts, the values added later (right-most) overwrite previous values. - -### Use template tags inside component inputs - -_New in version 0.93_ - -When passing data around, sometimes you may need to do light transformations, like negating booleans or filtering lists. - -Normally, what you would have to do is to define ALL the variables -inside `get_context_data()`. But this can get messy if your components contain a lot of logic. +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 -@register("calendar") -class Calendar(Component): - def get_context_data(self, id: str, editable: bool): - return { - "editable": editable, - "readonly": not editable, - "input_id": f"input-{id}", - "icon_id": f"icon-{id}", - ... - } +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", + ), +) ``` -Instead, template tags in django_components (`{% component %}`, `{% slot %}`, `{% provide %}`, etc) allow you to treat literal string values as templates: +### Extensions -```django -{% component 'blog_post' - "As positional arg {# yay #}" - title="{{ person.first_name }} {{ person.last_name }}" - id="{% random_int 10 20 %}" - readonly="{{ editable|not }}" - author="John Wick {# TODO: parametrize #}" -/ %} -``` +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: -In the example above: -- Component `test` receives a positional argument with value `"As positional arg "`. The comment is omitted. -- Kwarg `title` is passed as a string, e.g. `John Doe` -- Kwarg `id` is passed as `int`, e.g. `15` -- Kwarg `readonly` is passed as `bool`, e.g. `False` -- Kwarg `author` is passed as a string, e.g. `John Wick ` (Comment omitted) +- 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 -This is inspired by [django-cotton](https://github.com/wrabit/django-cotton#template-expressions-in-attributes). +Some of the extensions include: -#### Passing data as string vs original values +- [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) -Sometimes you may want to use the template tags to transform -or generate the data that is then passed to the component. +Some of the planned extensions include: -The data doesn't necessarily have to be strings. In the example above, the kwarg `id` was passed as an integer, NOT a string. +- AlpineJS integration +- Storybook integration +- Component-level benchmarking with asv -Although the string literals for components inputs are treated as regular Django templates, there is one special case: +### Caching -When the string literal contains only a single template tag, with no extra text, then the value is passed as the original type instead of a string. - -Here, `page` is an integer: - -```django -{% component 'blog_post' page="{% random_int 10 20 %}" / %} -``` - -Here, `page` is a string: - -```django -{% component 'blog_post' page=" {% random_int 10 20 %} " / %} -``` - -And same applies to the `{{ }}` variable tags: - -Here, `items` is a list: - -```django -{% component 'cat_list' items="{{ cats|slice:':2' }}" / %} -``` - -Here, `items` is a string: - -```django -{% component 'cat_list' items="{{ cats|slice:':2' }} See more" / %} -``` - -#### Evaluating Python expressions in template - -You can even go a step further and have a similar experience to Vue or React, -where you can evaluate arbitrary code expressions: - -```jsx - -``` - -Similar is possible with [`django-expr`](https://pypi.org/project/django-expr/), which adds an `expr` tag and filter that you can use to evaluate Python expressions from within the template: - -```django -{% component "my_form" - value="{% expr 'input_value if is_enabled else None' %}" -/ %} -``` - -> Note: Never use this feature to mix business logic and template logic. Business logic should still be in the view! - -### Pass dictonary by its key-value pairs - -_New in version 0.74_: - -Sometimes, a component may expect a dictionary as one of its inputs. - -Most commonly, this happens when a component accepts a dictionary -of HTML attributes (usually called `attrs`) to pass to the underlying template. - -In such cases, we may want to define some HTML attributes statically, and other dynamically. -But for that, we need to define this dictionary on Python side: +- [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 -@register("my_comp") -class MyComp(Component): - template = """ - {% component "other" attrs=attrs / %} - """ +from django_components import Component - def get_context_data(self, some_id: str): - attrs = { - "class": "pa-4 flex", - "data-some-id": some_id, - "@click.stop": "onClickHandler", - } - return {"attrs": attrs} +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)}") ``` -But as you can see in the case above, the event handler `@click.stop` and styling `pa-4 flex` -are disconnected from the template. If the component grew in size and we moved the HTML -to a separate file, we would have hard time reasoning about the component's template. +### Simple testing -Luckily, there's a better way. +- 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). -When we want to pass a dictionary to a component, we can define individual key-value pairs -as component kwargs, so we can keep all the relevant information in the template. For that, -we prefix the key with the name of the dict and `:`. So key `class` of input `attrs` becomes -`attrs:class`. And our example becomes: +```python +from django_components.testing import djc_test -```py -@register("my_comp") -class MyComp(Component): - template = """ - {% component "other" - attrs:class="pa-4 flex" - attrs:data-some-id=some_id - attrs:@click.stop="onClickHandler" - / %} - """ +from components.my_table import MyTable - def get_context_data(self, some_id: str): - return {"some_id": some_id} -``` - -Sweet! Now all the relevant HTML is inside the template, and we can move it to a separate file with confidence: - -```django -{% component "other" - attrs:class="pa-4 flex" - attrs:data-some-id=some_id - attrs:@click.stop="onClickHandler" -/ %} -``` - -> Note: It is NOT possible to define nested dictionaries, so -> `attrs:my_key:two=2` would be interpreted as: -> -> ```py -> {"attrs": {"my_key:two": 2}} -> ``` - -## Prop drilling and dependency injection (provide / inject) - -_New in version 0.80_: - -Django components supports dependency injection with the combination of: - -1. `{% provide %}` tag -1. `inject()` method of the `Component` class - -### What is "dependency injection" and "prop drilling"? - -Prop drilling refers to a scenario in UI development where you need to pass data through many layers of a component tree to reach the nested components that actually need the data. - -Normally, you'd use props to send data from a parent component to its children. However, this straightforward method becomes cumbersome and inefficient if the data has to travel through many levels or if several components scattered at different depths all need the same piece of information. - -This results in a situation where the intermediate components, which don't need the data for their own functioning, end up having to manage and pass along these props. This clutters the component tree and makes the code verbose and harder to manage. - -A neat solution to avoid prop drilling is using the "provide and inject" technique, AKA dependency injection. - -With dependency injection, a parent component acts like a data hub for all its descendants. This setup allows any component, no matter how deeply nested it is, to access the required data directly from this centralized provider without having to messily pass props down the chain. This approach significantly cleans up the code and makes it easier to maintain. - -This feature is inspired by Vue's [Provide / Inject](https://vuejs.org/guide/components/provide-inject) and React's [Context / useContext](https://react.dev/learn/passing-data-deeply-with-context). - -### How to use provide / inject - -As the name suggest, using provide / inject consists of 2 steps - -1. Providing data -2. Injecting provided data - -For examples of advanced uses of provide / inject, [see this discussion](https://github.com/EmilStenstrom/django-components/pull/506#issuecomment-2132102584). - -### Using `{% provide %}` tag - -First we use the `{% provide %}` tag to define the data we want to "provide" (make available). - -```django -{% provide "my_data" key="hi" another=123 %} - {% component "child" / %} <--- Can access "my_data" -{% endprovide %} - -{% component "child" / %} <--- Cannot access "my_data" -``` - -Notice that the `provide` tag REQUIRES a name as a first argument. This is the _key_ by which we can then access the data passed to this tag. - -`provide` tag name must resolve to a valid identifier (AKA a valid Python variable name). - -Once you've set the name, you define the data you want to "provide" by passing it as keyword arguments. This is similar to how you pass data to the `{% with %}` tag. - -> NOTE: Kwargs passed to `{% provide %}` are NOT added to the context. -> In the example below, the `{{ key }}` won't render anything: -> -> ```django -> {% provide "my_data" key="hi" another=123 %} -> {{ key }} -> {% endprovide %} -> ``` - -Similarly to [slots and fills](#dynamic-slots-and-fills), also provide's name argument can be set dynamically via a variable, a template expression, or a spread operator: - -```django -{% provide name=name ... %} - ... -{% provide %} - -``` - -### Using `inject()` method - -To "inject" (access) the data defined on the `provide` tag, you can use the `inject()` method inside of `get_context_data()`. - -For a component to be able to "inject" some data, the component (`{% component %}` tag) must be nested inside the `{% provide %}` tag. - -In the example from previous section, we've defined two kwargs: `key="hi" another=123`. That means that if we now inject `"my_data"`, we get an object with 2 attributes - `key` and `another`. - -```py -class ChildComponent(Component): - def get_context_data(self): - my_data = self.inject("my_data") - print(my_data.key) # hi - print(my_data.another) # 123 - return {} -``` - -First argument to `inject` is the _key_ (or _name_) of the provided data. This -must match the string that you used in the `provide` tag. If no provider -with given key is found, `inject` raises a `KeyError`. - -To avoid the error, you can pass a second argument to `inject` to which will act as a default value, similar to `dict.get(key, default)`: - -```py -class ChildComponent(Component): - def get_context_data(self): - my_data = self.inject("invalid_key", DEFAULT_DATA) - assert my_data == DEFAUKT_DATA - return {} -``` - -The instance returned from `inject()` is a subclass of `NamedTuple`, so the instance is immutable. This ensures that the data returned from `inject` will always -have all the keys that were passed to the `provide` tag. - -> NOTE: `inject()` works strictly only in `get_context_data`. If you try to call it from elsewhere, it will raise an error. - -### Full example - -```py -@register("child") -class ChildComponent(Component): - template = """ -
{{ my_data.key }}
-
{{ my_data.another }}
- """ - - def get_context_data(self): - my_data = self.inject("my_data", "default") - return {"my_data": my_data} - -template_str = """ - {% load component_tags %} - {% provide "my_data" key="hi" another=123 %} - {% component "child" / %} - {% endprovide %} -""" -``` - -renders: - -```html -
hi
-
123
-``` - -## Component context and scope - -By default, context variables are passed down the template as in regular Django - deeper scopes can access the variables from the outer scopes. So if you have several nested forloops, then inside the deep-most loop you can access variables defined by all previous loops. - -With this in mind, the `{% component %}` tag behaves similarly to `{% include %}` tag - inside the component tag, you can access all variables that were defined outside of it. - -And just like with `{% include %}`, if you don't want a specific component template to have access to the parent context, add `only` to the `{% component %}` tag: - -```htmldjango -{% component "calendar" date="2015-06-19" only / %} -``` - -NOTE: `{% csrf_token %}` tags need access to the top-level context, and they will not function properly if they are rendered in a component that is called with the `only` modifier. - -If you find yourself using the `only` modifier often, you can set the [context_behavior](#context-behavior) option to `"isolated"`, which automatically applies the `only` modifier. This is useful if you want to make sure that components don't accidentally access the outer context. - -Components can also access the outer context in their context methods like `get_context_data` by accessing the property `self.outer_context`. - -## Customizing component tags with TagFormatter - -_New in version 0.89_ - -By default, components are rendered using the pair of `{% component %}` / `{% endcomponent %}` template tags: - -```django -{% component "button" href="..." disabled %} -Click me! -{% endcomponent %} - -{# or #} - -{% component "button" href="..." disabled / %} -``` - -You can change this behaviour in the settings under the [`COMPONENTS.tag_formatter`](#tag-formatter-setting). - -For example, if you set the tag formatter to `django_components.shorthand_component_formatter`, the components will use their name as the template tags: - -```django -{% button href="..." disabled %} - Click me! -{% endbutton %} - -{# or #} - -{% button href="..." disabled / %} -``` - -### Available TagFormatters - -django_components provides following predefined TagFormatters: - -- **`ComponentFormatter` (`django_components.component_formatter`)** - - Default - - Uses the `component` and `endcomponent` tags, and the component name is gives as the first positional argument. - - Example as block: - ```django - {% component "button" href="..." %} - {% fill "content" %} - ... - {% endfill %} - {% endcomponent %} - ``` - - Example as inlined tag: - ```django - {% component "button" href="..." / %} - ``` - -- **`ShorthandComponentFormatter` (`django_components.shorthand_component_formatter`)** - - Uses the component name as start tag, and `end` - as an end tag. - - Example as block: - ```django - {% button href="..." %} - Click me! - {% endbutton %} - ``` - - Example as inlined tag: - ```django - {% button href="..." / %} - ``` - -### Writing your own TagFormatter - -#### Background - -First, let's discuss how TagFormatters work, and how components are rendered in django_components. - -When you render a component with `{% component %}` (or your own tag), the following happens: -1. `component` must be registered as a Django's template tag -2. Django triggers django_components's tag handler for tag `component`. -3. The tag handler passes the tag contents for pre-processing to `TagFormatter.parse()`. - - So if you render this: - ```django - {% component "button" href="..." disabled %} - {% endcomponent %} - ``` - - Then `TagFormatter.parse()` will receive a following input: - ```py - ["component", '"button"', 'href="..."', 'disabled'] - ``` -4. `TagFormatter` extracts the component name and the remaining input. - - So, given the above, `TagFormatter.parse()` returns the following: - ```py - TagResult( - component_name="button", - tokens=['href="..."', 'disabled'] +@djc_test +def test_my_table(): + rendered = MyTable.render( + kwargs={ + "title": "My table", + }, ) - ``` -5. The tag handler resumes, using the tokens returned from `TagFormatter`. - - So, continuing the example, at this point the tag handler practically behaves as if you rendered: - ```django - {% component href="..." disabled %} - ``` -6. Tag handler looks up the component `button`, and passes the args, kwargs, and slots to it. - -#### TagFormatter - -`TagFormatter` handles following parts of the process above: -- Generates start/end tags, given a component. This is what you then call from within your template as `{% component %}`. - -- When you `{% component %}`, tag formatter pre-processes the tag contents, so it can link back the custom template tag to the right component. - -To do so, subclass from `TagFormatterABC` and implement following method: -- `start_tag` -- `end_tag` -- `parse` - -For example, this is the implementation of [`ShorthandComponentFormatter`](#available-tagformatters) - -```py -class ShorthandComponentFormatter(TagFormatterABC): - # Given a component name, generate the start template tag - def start_tag(self, name: str) -> str: - return name # e.g. 'button' - - # Given a component name, generate the start template tag - def end_tag(self, name: str) -> str: - return f"end{name}" # e.g. 'endbutton' - - # Given a tag, e.g. - # `{% button href="..." disabled %}` - # - # The parser receives: - # `['button', 'href="..."', 'disabled']` - def parse(self, tokens: List[str]) -> TagResult: - tokens = [*tokens] - name = tokens.pop(0) - return TagResult( - name, # e.g. 'button' - tokens # e.g. ['href="..."', 'disabled'] - ) + assert rendered == "My table
" ``` -That's it! And once your `TagFormatter` is ready, don't forget to update the settings! +### Debugging features -## Defining HTML/JS/CSS files +- **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. -django_component's management of files builds on top of [Django's `Media` class](https://docs.djangoproject.com/en/5.0/topics/forms/media/). +
+Component debugging visualization showing slot highlighting +
-To be familiar with how Django handles static files, we recommend reading also: +### Sharing components -- [How to manage static files (e.g. images, JavaScript, CSS)](https://docs.djangoproject.com/en/5.0/howto/static-files/) - -### Defining file paths relative to component or static dirs - -As seen in the [getting started example](#create-your-first-component), to associate HTML/JS/CSS -files with a component, you set them as `template_name`, `Media.js` and `Media.css` respectively: - -```py -# In a file [project root]/components/calendar/calendar.py -from django_components import Component, register - -@register("calendar") -class Calendar(Component): - template_name = "template.html" - - class Media: - css = "style.css" - js = "script.js" -``` - -In the example above, the files are defined relative to the directory where `component.py` is. - -Alternatively, you can specify the file paths relative to the directories set in `STATICFILES_DIRS`. - -Assuming that `STATICFILES_DIRS` contains path `[project root]/components`, we can rewrite the example as: - -```py -# In a file [project root]/components/calendar/calendar.py -from django_components import Component, register - -@register("calendar") -class Calendar(Component): - template_name = "calendar/template.html" - - class Media: - css = "calendar/style.css" - js = "calendar/script.js" -``` - -NOTE: In case of conflict, the preference goes to resolving the files relative to the component's directory. - -### Defining multiple paths - -Each component can have only a single template. However, you can define as many JS or CSS files as you want using a list. - -```py -class MyComponent(Component): - class Media: - js = ["path/to/script1.js", "path/to/script2.js"] - css = ["path/to/style1.css", "path/to/style2.css"] -``` - -### Configuring CSS Media Types - -You can define which stylesheets will be associated with which -[CSS Media types](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_media_queries/Using_media_queries#targeting_media_types). You do so by defining CSS files as a dictionary. - -See the corresponding [Django Documentation](https://docs.djangoproject.com/en/5.0/topics/forms/media/#css). - -Again, you can set either a single file or a list of files per media type: - -```py -class MyComponent(Component): - class Media: - css = { - "all": "path/to/style1.css", - "print": "path/to/style2.css", - } -``` - -```py -class MyComponent(Component): - class Media: - css = { - "all": ["path/to/style1.css", "path/to/style2.css"], - "print": ["path/to/style3.css", "path/to/style4.css"], - } -``` - -NOTE: When you define CSS as a string or a list, the `all` media type is implied. - -### Supported types for file paths - -File paths can be any of: - -- `str` -- `bytes` -- `PathLike` (`__fspath__` method) -- `SafeData` (`__html__` method) -- `Callable` that returns any of the above, evaluated at class creation (`__new__`) - -```py -from pathlib import Path - -from django.utils.safestring import mark_safe - -class SimpleComponent(Component): - class Media: - css = [ - mark_safe(''), - Path("calendar/style1.css"), - "calendar/style2.css", - b"calendar/style3.css", - lambda: "calendar/style4.css", - ] - js = [ - mark_safe(''), - Path("calendar/script1.js"), - "calendar/script2.js", - b"calendar/script3.js", - lambda: "calendar/script4.js", - ] -``` - -### Path as objects - -In the example [above](#supported-types-for-file-paths), you could see that when we used `mark_safe` to mark a string as a `SafeString`, we had to define the full `' - ) - -@register("calendar") -class Calendar(Component): - template_name = "calendar/template.html" - - def get_context_data(self, date): - return { - "date": date, - } - - class Media: - css = "calendar/style.css" - js = [ - # ', - self.absolute_path(path) - ) - return tags - -@register("calendar") -class Calendar(Component): - template_name = "calendar/template.html" - - class Media: - css = "calendar/style.css" - js = "calendar/script.js" - - # Override the behavior of Media class - media_class = MyMedia -``` - -NOTE: The instance of the `Media` class (or it's subclass) is available under `Component.media` after the class creation (`__new__`). - -## Rendering JS/CSS dependencies - -The JS and CSS files included in components are not automatically rendered. -Instead, use the following tags to specify where to render the dependencies: - -- `component_dependencies` - Renders both JS and CSS -- `component_js_dependencies` - Renders only JS -- `component_css_dependencies` - Reneders only CSS - -JS files are rendered as `""" - - -@override_settings(COMPONENTS={"RENDER_DEPENDENCIES": True}) -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_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/CHANGELOG.md b/docs/CHANGELOG.md deleted file mode 100644 index 2e473032..00000000 --- a/docs/CHANGELOG.md +++ /dev/null @@ -1,13 +0,0 @@ ---- -hide: - - toc ---- - -# Release notes - -{! - include-markdown "../README.md" - start="## Release notes" - end='## ' - heading-offset=1 -!} \ No newline at end of file diff --git a/docs/README.md b/docs/README.md index 612c7a5e..74b225f9 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1 +1,6 @@ ---8<-- "README.md" +--- +title: Welcome to Django Components +weight: 1 +--- + +--8<-- "docs/overview/welcome.md:4" diff --git a/docs/SUMMARY.md b/docs/SUMMARY.md deleted file mode 100644 index 18286a21..00000000 --- a/docs/SUMMARY.md +++ /dev/null @@ -1,6 +0,0 @@ -* [README](README.md) - * [Changelog](CHANGELOG.md) - * [Code of Conduct](CODE_OF_CONDUCT.md) - * [License](license.md) -* Reference - * [API Reference](reference/) \ No newline at end of file diff --git a/docs/__init__.py b/docs/__init__.py new file mode 100644 index 00000000..e69de29b 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 = $('