2025, Oct 19 22:31

Django में format_html_join के साथ dict पर KeyError: कारण और 3 तरीके

जानें क्यों Django के पुराने वर्ज़न में format_html_join() को dict देने पर KeyError आता है, और इसे सुलझाने के 3 तरीके: Django 5.2+ अपग्रेड, छोटा compat शिम, या positional tuples.

Django के format_html_join() से डिक्शनरियों की सूची को तालिका में बदलना पहली नज़र में सरल लगता है, लेकिन मैपिंग्स पास करते समय कई लोग KeyError से टकराते हैं। असली वजह न तो जेनरेटर है, न ही डेटा की संरचना — मुद्दा Django के संस्करण और format_html_join() के इनपुट प्रोसेस करने के पुराने तरीके में है।

समस्या का विवरण

आपके पास एक मॉडल मेथड है जो वर्ज़न हिस्ट्री को डिक्शनरीज़ की सूची के रूप में बनाता है, और आप उसे format_html_join() से सुंदर HTML तालिका में बदलना चाहते हैं। डेटा ऐसा है: हर पंक्ति में question, answer, user और timestamp कुंजियाँ हैं। लेकिन format_html_join() कॉल KeyError: 'question' फेंक देता है।

दोहराने योग्य कोड उदाहरण

नीचे सेटअप दिखाया गया है: एक हिस्ट्री कलेक्टर और एक HTML जनरेटर। लॉजिक न्यूनतम है और डेटा स्कीमा आपके HTML प्लेसहोल्डर्स से मेल खाता है।

from django.utils.html import format_html_join
class SomeModel:
    def collect_history(self):
        records = Version.objects.get_for_object(self)
        rows = []
        for rec in records:
            snapshot = rec.field_dict
            item = {
                "question": snapshot["question"],
                "answer": snapshot["answer"],
                "user": rec.revision.user.username,
                "timestamp": rec.revision.date_created.strftime("%Y-%m-%d %H:%M"),
            }
            rows.append(item)
        return rows
    def render_history_html(self):
        table_html = format_html_join(
            "\n",
            """<tr>
                <td>{question}</td>
                <td>{answer}</td>
                <td>{user}</td>
                <td>{timestamp}</td>
            </tr>""",
            self.collect_history()
        )
        return table_html

यह सेटअप पहले प्लेसहोल्डर के नाम के साथ KeyError उठाता है, जैसे KeyError: 'question'.

ऐसा क्यों होता है

समस्या यह नहीं है कि डिक्शनरी की सूची iterable नहीं है। असल व्यवहार संस्करण पर निर्भर है। Django 5.2 से पहले format_html_join() अंदरूनी तौर पर केवल positional आर्ग्युमेंट्स format_html() को भेजता था, इसलिए जब आप dict देते हैं तो आपके फॉर्मैट स्ट्रिंग के नामित प्लेसहोल्डर्स हल नहीं हो पाते। दूसरे शब्दों में, मैपिंग्स को keyword आर्ग्युमेंट्स की तरह अनपैक नहीं किया जाता था। सोर्स कुछ यूँ था:

return mark_safe(
    conditional_escape(sep).join(
        format_html(format_string, *args) for args in args_generator
    )
)

मैपिंग्स का सपोर्ट हाल ही में जोड़ा गया और Django 5.2 में जारी किया गया। रिलीज़ नोट्स में कहा गया है:

format_html_join() अब मैपिंग्स की iterable स्वीकार करता है और उनकी सामग्री को format_html() में keyword आर्ग्युमेंट्स की तरह पास करता है।

व्यावहारिक समाधान

आपकी परिस्थितियों पर निर्भर करते हुए, इसे काम में लाने के तीन आसान तरीक़े हैं।

विकल्प 1: Django 5.2 या नया संस्करण इस्तेमाल करें

यदि आप Django 5.2+ पर हैं, तो आपकी मूल योजना सीधे काम करेगी: dict की iterable पास करें और नामित प्लेसहोल्डर्स उपयोग करें। ऊपर दिए उदाहरण का वही render मेथड मनचाहा HTML देगा। और किसी बदलाव की ज़रूरत नहीं।

विकल्प 2: छोटा-सा कम्पैटिबिलिटी शिम जोड़ें

अगर अभी अपग्रेड संभव नहीं है, तो आप एक छोटा-सा हेल्पर बना सकते हैं जो सीक्वेन्स और मैपिंग — दोनों को संभालता है। यह 5.2 में आए व्यवहार को दर्शाता है और आपके डेटा स्ट्रक्चर को जस का तस रखता है।

from collections.abc import Mapping
from django.utils.html import conditional_escape, format_html, mark_safe
def format_html_join_compat(sep, fmt, arg_iter):
    return mark_safe(
        conditional_escape(sep).join(
            (
                format_html(fmt, **args)
                if isinstance(args, Mapping)
                else format_html(fmt, *args)
            )
            for args in arg_iter
        )
    )

इस हेल्पर के साथ, रेंडरिंग मेथड अभिव्यक्तिपूर्ण रहता है और मैपिंग्स के लिए सुरक्षित भी:

def render_history_html(self):
    return format_html_join_compat(
        "\n",
        """<tr>
            <td>{question}</td>
            <td>{answer}</td>
            <td>{user}</td>
            <td>{timestamp}</td>
        </tr>""",
        self.collect_history()
    )

विकल्प 3: positional आर्ग्युमेंट्स पर जाएँ

एक और तरीका है कि आप पास किए जाने वाले डेटा को positional tuples में बदलें और HTML अंश में positional प्लेसहोल्डर्स इस्तेमाल करें। इससे मैपिंग्स की आवश्यकता नहीं रहती और पुरानी format_html_join() के साथ भी काम हो जाता है।

from operator import itemgetter
from django.utils.html import format_html_join
def render_history_html(self):
    return format_html_join(
        "\n",
        '<tr>' + '<tr>{}</td>'*4 + '</tr>',
        map(
            itemgetter('question', 'answer', 'user', 'timestamp'),
            self.collect_history()
        ),
    )

यह जानना क्यों ज़रूरी है

Django में सर्वर-साइड HTML बनाते समय अक्सर format_html() और format_html_join() पर भरोसा किया जाता है ताकि एस्केपिंग सुरक्षित रहे और Python के भीतर साफ टेम्पलेटिंग मिल सके। आर्ग्युमेंट संभालने में छोटी-सी भिन्नता भी ऐसे उलझाऊ एरर दे सकती है जो डेटा की गड़बड़ी लगती हैं, जबकि असल में यह API की क्षमता का असंगति होता है। यह समझना कि नामित प्लेसहोल्डर्स कब keyword आर्ग्युमेंट्स माँगते हैं — और आपका फ्रेमवर्क संस्करण मैपिंग्स को सपोर्ट करता है या नहीं — समय बचाता है और ठीक-ठाक डेटा स्ट्रक्चर को अनावश्यक रूप से फिर से लिखने की चाहत कम करता है।

मुख्य बातें

यदि आप format_html_join() में dicts पास कर रहे हैं और प्लेसहोल्डर नाम पर KeyError दिख रहा है, तो पहले अपना Django संस्करण जाँचें। 5.2 या नए में मैपिंग्स की iterable समर्थित है और आपकी dicts की सूची अपेक्षित रूप से काम करेगी। यदि आप पुराने रिलीज़ पर हैं, तो या तो एक छोटा-सा कम्पैटिबिलिटी रैपर जोड़ें जो मैपिंग्स को keyword आर्ग्युमेंट्स की तरह अनपैक करे, या अपनी dicts को positional tuples में मैप करें और टेम्पलेट को positional प्लेसहोल्डर्स पर स्विच करें। हर तरीका लॉजिक को साफ रखता है और आउटपुट को सुरक्षित।

यह लेख StackOverflow के प्रश्न (लेखक: robline) और willeM_ Van Onsem के उत्तर पर आधारित है।