2025, Oct 05 05:31
TYPE_CHECKING वाले imports के साथ get_type_hints को सफलतापूर्वक resolve करने का तरीका
Python में type hints से स्कीमा बनाते समय TYPE_CHECKING वाले imports से get_type_hints क्यों टूटता है, और libcst से लोड कर globalns में देकर इसे कैसे ठीक करें।
Python के type hints से रनटाइम स्कीमा बनाना आम तौर पर सहज रहता है, जब तक कि आप TYPE_CHECKING से सुरक्षित की गई annotations का सामना नहीं करते। उस क्षण, get_type_hints के जरिए introspection विफल होने लगता है, क्योंकि जिन नामों पर आप भरोसा करते हैं, वे रनटाइम पर होते ही नहीं। नीचे इस विफलता के पैटर्न का व्यावहारिक विवरण और बिना मूल सोर्स बदले get_type_hints को उन annotations से अवगत कराने का ठोस तरीका दिया गया है।
समस्या का सार
आपके पास एक क्लास अलग फाइल में परिभाषित है। इसकी मेथड्स को उन टाइप्स से annotate किया गया है जो केवल if TYPE_CHECKING ब्लॉक के अंदर import होते हैं। इसके बाद आप उस फाइल को डायनामिकली लोड करके उसकी functions से type hints निकालने की कोशिश करते हैं। get_type_hints कॉल टूट जाती है, क्योंकि annotations में उपयोग किए गए नाम रनटाइम पर उपलब्ध ही नहीं हैं।
न्यूनतम असफल उदाहरण
क्लास एक अलग मॉड्यूल में है और ऐसा import उपयोग करता है जो केवल टाइप चेकिंग के लिए मौजूद है:
from typing import TYPE_CHECKING
if TYPE_CHECKING:
    import qxx
class Gadget:
    def do_work(self) -> qxx: ...
कहीं और आप मॉड्यूल import करते हैं, functions ढूंढते हैं और उनकी annotations हल करने का प्रयास करते हैं:
from importlib import import_module
import inspect
from typing import get_type_hints
mod_obj = import_module("gadget_module")
for fn in inspect.getmembers(Gadget, predicate=inspect.isfunction).values():
    get_type_hints(fn, globalns=mod_obj.__dict__)
यह इसलिए विफल होता है क्योंकि qxx अपरिभाषित है। TYPE_CHECKING से सुरक्षित import रनटाइम पर कभी चलता ही नहीं, इसलिए get_type_hints नाम resolve नहीं कर पाता। जैसा कि अन्यत्र भी बताया गया है, get_type_hints forward references को resolve करने की कोशिश करता है और असफल होने पर त्रुटि उठाता है—यहीं वही हो रहा है।
यह क्यों होता है
TYPE_CHECKING imports को इस तरह नियंत्रित करता है कि वे टाइप चेकर द्वारा तो मूल्यांकित हों, पर रनटाइम पर नहीं। जब get_type_hints annotations का मूल्यांकन करता है, तो उसे संदर्भित सभी नाम दिए गए globalns या localns में चाहिए होते हैं। चूंकि qxx रनटाइम पर कभी import नहीं होता, प्रतीक मौजूद नहीं रहता और resolution विफल हो जाती है। यह get_type_hints के इस व्यवहार से गहरे जुड़ा है कि वह annotations (forward references सहित) को उत्सुकता से resolve करता है।
व्यावहारिक उपाय: केवल TYPE_CHECKING वाले imports पहले से लोड करें और उन्हें get_type_hints को दें
विश्वसनीय तरीका यह है कि लक्ष्य फाइल को पार्स करें, if TYPE_CHECKING के अंदर रखे imports का पता लगाएं, उन मॉड्यूल्स या ऑब्जेक्ट्स को स्वयं import करें, और फिर globalns के जरिए उस मैपिंग को get_type_hints में पास करें। इससे मूल सोर्स अपरिवर्तित रहता है और get_type_hints को resolution के लिए जरूरी सभी नाम मिल जाते हैं।
नीचे दिया गया तरीका libcst का उपयोग करता है ताकि TYPE_CHECKING ब्लॉक के भीतर के imports मिल सकें और उन्हें एक dictionary में materialize किया जा सके, जो alias नामों को आयातित ऑब्जेक्ट्स या मॉड्यूल्स से मैप करती है।
import importlib
from pathlib import Path
from typing import Any
import libcst as cst
def collect_typing_guarded_imports(file_loc: str) -> dict[str, Any]:
    """
    Locate all imports under an `if TYPE_CHECKING:` block in the given file
    and import them so they can be injected into get_type_hints.
    """
    class ImportAccumulator(cst.CSTVisitor):
        def __init__(self) -> None:
            self.bucket: list[cst.Import | cst.ImportFrom] = []
        def visit_Import(self, node: cst.Import) -> None:
            self.bucket.append(node)
        def visit_ImportFrom(self, node: cst.ImportFrom) -> None:
            self.bucket.append(node)
    class TCImportsScanner(cst.CSTVisitor):
        def __init__(self) -> None:
            self.matches: list[cst.Import | cst.ImportFrom] = []
        def visit_If(self, node: cst.If) -> bool:
            if isinstance(node.test, cst.Name) and node.test.value == "TYPE_CHECKING":
                acc = ImportAccumulator()
                node.body.visit(acc)
                self.matches.extend(acc.bucket)
                return False
            return True
    unit = cst.parse_module(Path(file_loc).read_text(encoding="utf8"))
    scout = TCImportsScanner()
    unit.visit(scout)
    resolved: dict[str, Any] = {}
    for item in scout.matches:
        if isinstance(item, cst.Import):
            # pandas को pd के रूप में import करें
            for alias in item.names:
                mod_name = unit.code_for_node(alias.name)
                imported = importlib.import_module(mod_name)
                alias_name = (
                    unit.code_for_node(alias.asname.name)
                    if alias.asname
                    else mod_name
                )
                resolved[alias_name] = imported
        else:
            # dataclasses से dataclass को dc नाम से import करें
            pkg_name = unit.code_for_node(item.module)
            imported_pkg = importlib.import_module(pkg_name)
            for alias in item.names:
                origin = unit.code_for_node(alias.name)
                alias_name = (
                    unit.code_for_node(alias.asname.name)
                    if alias.asname
                    else origin
                )
                resolved[alias_name] = getattr(imported_pkg, origin)
    return resolved
जब आपके पास यह मैपिंग आ जाए, तो इसे मॉड्यूल के globals के साथ मिलाकर get_type_hints को पास करें। इससे रिजॉल्वर को वे सभी नाम दिखने लगते हैं जो अन्यथा केवल स्टैटिक टाइप चेकिंग के दौरान मौजूद होते।
from importlib import import_module
import inspect
from typing import get_type_hints
# उस मॉड्यूल को लोड करें जो TYPE_CHECKING से सुरक्षित imports के साथ क्लास परिभाषित करता है
mod_obj = import_module("gadget_module")
# उसी फाइल में केवल TYPE_CHECKING के तहत आने वाले अतिरिक्त नामों को खोजें
extra_ns = collect_typing_guarded_imports(mod_obj.__file__)
# दोनों namespace मिलाएं और hints को सफलतापूर्वक resolve करें
merged_ns = {**mod_obj.__dict__, **extra_ns}
for fn in inspect.getmembers(Gadget, predicate=inspect.isfunction).values():
    hints = get_type_hints(fn, globalns=merged_ns)
    # अपने schema बनाने के लिए `hints` का उपयोग करें
यह क्यों मायने रखता है
जब स्कीमा जनरेशन या कोई भी रनटाइम introspection annotations पर निर्भर करता है, तो TYPE_CHECKING के तहत छूटा हुआ import पूरी पाइपलाइन को पटरी से उतार सकता है। resolution को स्पष्ट बनाना आपके टूलिंग को टिकाऊ रखता है। यह मानकर चलता है कि annotations ऐसे नामों का संदर्भ दे सकते हैं जो रनटाइम पर उपलब्ध नहीं हैं, और फिर भी प्रक्रिया को मूल सोर्स बदले बिना पूरा कराता है। जैसा ठीक कहा गया है, get_type_hints forward references को resolve करने की कोशिश करता है और न हो पाने पर त्रुटि उठाता है, इसलिए resolution context को पहले से भरना महत्वपूर्ण है। इस क्षेत्र में PEP 563 और 749 पर चर्चा है, पर ऊपर दिया गया तरीका आपके मौजूदा कोड के साथ वर्तमान resolution को सफल कराने पर केंद्रित रहता है।
निष्कर्ष
अगर आपको ऐसी annotations resolve करनी हैं जो केवल TYPE_CHECKING वाले imports पर निर्भर हैं, तो पहले उन्हें प्रोग्रामेटिक तरीके से निकालकर लोड करें, फिर globalns के माध्यम से get_type_hints को दें। यह छोटा सा कदम आपके स्कीमा जनरेशन को भरोसेमंद बना देता है, बिना विश्लेषण किए जा रहे मॉड्यूल को छुए। सोर्स और टूलिंग के बीच स्पष्ट विभाजन रखें: सोर्स में TYPE_CHECKING गार्ड रहने दें, और टूलिंग से रनटाइम का अंतर पाटें।